// src/components/job/JobDetail.js - コールバック修正版
import React, { useState, useEffect, useContext, useCallback } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import styled from 'styled-components';
import { JobPostings, Applications } from '../../api/api';
import { AuthContext } from '../../context/AuthContext';
import Button from '../common/Button';
import Card from '../common/Card';
import JobLocationMap from '../map/JobLocationMap';
// 既存のスタイルコンポーネントはそのまま使用
const JobDetailContainer = styled.div`
max-width: 800px;
margin: 40px auto;
padding: 0 24px;
`;
const JobTitle = styled.h1`
text-align: center;
margin-bottom: 24px;
color: var(--primary-color);
`;
const JobContent = styled.div`
margin-top: 32px;
`;
const JobSection = styled.div`
margin-bottom: 24px;
`;
const SectionTitle = styled.h2`
font-size: 22px;
margin-bottom: 16px;
color: var(--secondary-color);
`;
const JobDetailItem = styled.p`
margin-bottom: 12px;
font-size: 18px;
line-height: 1.6;
`;
const JobLabel = styled.span`
font-weight: 700;
margin-right: 8px;
`;
const ButtonGroup = styled.div`
display: flex;
justify-content: center;
gap: 16px;
margin-top: 32px;
@media (max-width: 768px) {
flex-direction: column;
gap: 8px;
}
`;
const ErrorMessage = styled.p`
color: var(--error-color);
font-size: 16px;
margin-top: 16px;
text-align: center;
`;
const SuccessMessage = styled.p`
color: var(--success-color);
font-size: 18px;
font-weight: 700;
margin-top: 16px;
text-align: center;
`;
const Loading = styled.p`
text-align: center;
font-size: 18px;
color: var(--text-light);
margin: 48px 0;
`;
const Error = styled.p`
text-align: center;
font-size: 18px;
color: var(--error-color);
margin: 48px 0;
`;
const Modal = styled.div`
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
`;
const ModalContent = styled(Card)`
width: 90%;
max-width: 500px;
padding: 32px;
text-align: center;
`;
const ModalTitle = styled.h2`
margin-bottom: 24px;
color: var(--primary-color);
`;
const ModalText = styled.p`
font-size: 18px;
margin-bottom: 24px;
`;
const MapSection = styled(JobSection)`
border: 1px solid #e0e0e0;
border-radius: var(--border-radius);
padding: 20px;
background: #fafafa;
`;
const LocationInfo = styled.div`
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
font-size: 16px;
color: var(--text-color);
`;
const JobDetail = () => {
const { id } = useParams();
const { currentUser } = useContext(AuthContext);
const navigate = useNavigate();
const [job, setJob] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [successMessage, setSuccessMessage] = useState('');
const [errorMessage, setErrorMessage] = useState('');
const [showConfirmModal, setShowConfirmModal] = useState(false);
const [locationData, setLocationData] = useState(null);
useEffect(() => {
const fetchJobDetail = async () => {
try {
const response = await JobPostings.getById(id);
setJob(response.data);
setLoading(false);
} catch (err) {
setError('求人情報の取得に失敗しました');
setLoading(false);
}
};
fetchJobDetail();
}, [id]);
const handleApply = () => {
if (!currentUser) {
navigate('/login');
return;
}
// 農家ユーザーは応募できない
if (currentUser.user_type === 'farmer') {
setErrorMessage('農家ユーザーは求人に応募できません');
return;
}
// 応募確認モーダルを表示
setShowConfirmModal(true);
};
const handleConfirmApply = async () => {
try {
await Applications.apply({
job_posting_id: job.id,
applicant_id: currentUser.id
});
setSuccessMessage('応募が完了しました!');
setShowConfirmModal(false);
// 応募完了後、プロフィールページにリダイレクト
setTimeout(() => {
navigate('/profile');
}, 2000);
} catch (err) {
setErrorMessage(err.response?.data?.error || '応募に失敗しました');
setShowConfirmModal(false);
}
};
const handleCancelApply = () => {
setShowConfirmModal(false);
};
const handleBack = () => {
navigate('/jobs');
};
// 🔧 修正: useCallbackで安定化したコールバック関数
const handleLocationFound = useCallback((result) => {
setLocationData(result);
console.log('📍 求人地の位置情報を取得:', result);
}, []);
const handleLocationError = useCallback((error) => {
console.warn('📍 求人地の位置情報取得エラー:', error);
}, []);
if (loading) {
return (
<JobDetailContainer>
<Loading>読み込み中...</Loading>
</JobDetailContainer>
);
}
if (error) {
return (
<JobDetailContainer>
<Error>{error}</Error>
<ButtonGroup>
<Button onClick={handleBack}>求人一覧に戻る</Button>
</ButtonGroup>
</JobDetailContainer>
);
}
if (!job) {
return (
<JobDetailContainer>
<Error>求人情報が見つかりませんでした</Error>
<ButtonGroup>
<Button onClick={handleBack}>求人一覧に戻る</Button>
</ButtonGroup>
</JobDetailContainer>
);
}
return (
<JobDetailContainer>
<Card>
<JobTitle>{job.company_name}</JobTitle>
<JobContent>
<JobSection>
<SectionTitle>勤務情報</SectionTitle>
<JobDetailItem>
<JobLabel>勤務地:</JobLabel>
{job.location}
</JobDetailItem>
<JobDetailItem>
<JobLabel>時給:</JobLabel>
{job.hourly_wage}円
</JobDetailItem>
<JobDetailItem>
<JobLabel>勤務曜日:</JobLabel>
{job.work_days}
</JobDetailItem>
<JobDetailItem>
<JobLabel>勤務時間:</JobLabel>
{job.work_hours}
</JobDetailItem>
{job.seasonal_vegetables && (
<JobDetailItem>
<JobLabel>旬の野菜:</JobLabel>
{job.seasonal_vegetables}
</JobDetailItem>
)}
<JobDetailItem>
<JobLabel>募集人数:</JobLabel>
{job.positions_available}人 (残り{job.positions_available - job.positions_filled}人)
</JobDetailItem>
</JobSection>
{/* 🔧 修正: 改良されたマップセクション */}
<MapSection>
<SectionTitle>📍 勤務地マップ</SectionTitle>
<LocationInfo>
<span>🏠</span>
<span>{job.location}</span>
{locationData && (
<span style={{ color: '#666', fontSize: '14px' }}>
(精度: {Math.round(locationData.accuracy)}%)
</span>
)}
</LocationInfo>
<JobLocationMap
address={job.location}
height="350px"
zoom={15}
showAccuracy={true}
enableRetry={true}
onLocationFound={handleLocationFound}
onLocationError={handleLocationError}
/>
{locationData && locationData.note && (
<div style={{
fontSize: '14px',
color: '#666',
marginTop: '8px',
fontStyle: 'italic'
}}>
ℹ️ {locationData.note}
</div>
)}
</MapSection>
<JobSection>
<SectionTitle>業務内容</SectionTitle>
<JobDetailItem>{job.job_description}</JobDetailItem>
</JobSection>
<JobSection>
<SectionTitle>連絡先</SectionTitle>
<JobDetailItem>
<JobLabel>連絡先:</JobLabel>
{job.contact}
</JobDetailItem>
</JobSection>
<ButtonGroup>
<Button onClick={handleApply} disabled={job.positions_filled >= job.positions_available}>
{job.positions_filled >= job.positions_available ? '募集終了' : '応募する'}
</Button>
<Button $secondary onClick={handleBack}>
求人一覧に戻る
</Button>
</ButtonGroup>
{errorMessage && <ErrorMessage>{errorMessage}</ErrorMessage>}
{successMessage && <SuccessMessage>{successMessage}</SuccessMessage>}
</JobContent>
</Card>
{showConfirmModal && (
<Modal>
<ModalContent>
<ModalTitle>応募の確認</ModalTitle>
<ModalText>
{job.company_name}の求人に応募します。よろしいですか?
</ModalText>
<ButtonGroup>
<Button onClick={handleConfirmApply}>応募する</Button>
<Button $secondary onClick={handleCancelApply}>キャンセル</Button>
</ButtonGroup>
</ModalContent>
</Modal>
)}
</JobDetailContainer>
);
};
export default JobDetail;