From 8b35290b48c96552c796fe336a280c08d32da959 Mon Sep 17 00:00:00 2001 From: Vincentius7 <124507621+Vincentius7@users.noreply.github.com> Date: Mon, 2 Jun 2025 05:51:22 +0900 Subject: [PATCH 01/21] =?UTF-8?q?dev=EB=A1=9C=20=EB=B0=B0=ED=8F=AC=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=206/2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c826207..dd32e25 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -8,7 +8,7 @@ on: - "backend/settings.gradle.kts" - "backend/Dockerfile" branches: - - main + - dev jobs: makeTagAndRelease: runs-on: ubuntu-latest From 21fff0754ce126510a4cafa2bea2987fd4f120fd Mon Sep 17 00:00:00 2001 From: vincentius Date: Mon, 2 Jun 2025 06:05:48 +0900 Subject: [PATCH 02/21] =?UTF-8?q?yml=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/petner/anidoc/AnidocApplication.java | 3 +++ .../domain/user/notification/service/NotificationService.java | 3 +++ backend/src/main/resources/application-prod.yml | 4 ++-- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/com/petner/anidoc/AnidocApplication.java b/backend/src/main/java/com/petner/anidoc/AnidocApplication.java index a89280f..9d8fa5a 100644 --- a/backend/src/main/java/com/petner/anidoc/AnidocApplication.java +++ b/backend/src/main/java/com/petner/anidoc/AnidocApplication.java @@ -4,11 +4,14 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import java.util.TimeZone; + @EnableJpaAuditing @SpringBootApplication public class AnidocApplication { public static void main(String[] args) { + TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul")); SpringApplication.run(AnidocApplication.class, args); } diff --git a/backend/src/main/java/com/petner/anidoc/domain/user/notification/service/NotificationService.java b/backend/src/main/java/com/petner/anidoc/domain/user/notification/service/NotificationService.java index 8133513..2d0a455 100644 --- a/backend/src/main/java/com/petner/anidoc/domain/user/notification/service/NotificationService.java +++ b/backend/src/main/java/com/petner/anidoc/domain/user/notification/service/NotificationService.java @@ -196,3 +196,6 @@ public long getUnreadCount(Long userId) { return notificationRepository.countByUserIdAndIsReadFalse(userId); } } + + + diff --git a/backend/src/main/resources/application-prod.yml b/backend/src/main/resources/application-prod.yml index 5e07cb1..31ce0f6 100644 --- a/backend/src/main/resources/application-prod.yml +++ b/backend/src/main/resources/application-prod.yml @@ -10,13 +10,13 @@ custom: spring: datasource: - url: jdbc:mysql://mysql_1:3306/anidocdb + url: jdbc:mysql://mysql_1:3306/anidocdb?serverTimezone=Asia/Seoul username: petner password: petner driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: - ddl-auto: update + ddl-auto: create properties: hibernate: format_sql: false From dfec8331b711963aef8945755ae647d920f06e0d Mon Sep 17 00:00:00 2001 From: Jaehyun Yoo Date: Mon, 2 Jun 2025 06:28:34 +0900 Subject: [PATCH 03/21] =?UTF-8?q?Feat=20:=20=EC=A7=84=EB=A3=8C=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/ReservationResponseDto.java | 3 + .../DoctorPetVaccineController.java | 31 +- .../dto/DoctorPetVaccineRequestDTO.java | 1 - .../vaccination/dto/VaccinationStatusDto.java | 15 + .../service/DoctorPetVaccineService.java | 42 +- frontend/src/app/medical-records/page.tsx | 38 +- frontend/src/components/Sidebar.tsx | 15 +- .../medical-records/StaffMedicalRecord.tsx | 296 +++++++++++--- .../VaccinationDetailModal.tsx | 370 ++++++++++++++++++ .../medical-records/VaccinationModal.tsx | 265 +++++++++++++ 10 files changed, 986 insertions(+), 90 deletions(-) create mode 100644 backend/src/main/java/com/petner/anidoc/domain/vet/vaccination/dto/VaccinationStatusDto.java create mode 100644 frontend/src/components/medical-records/VaccinationDetailModal.tsx create mode 100644 frontend/src/components/medical-records/VaccinationModal.tsx diff --git a/backend/src/main/java/com/petner/anidoc/domain/vet/reservation/dto/ReservationResponseDto.java b/backend/src/main/java/com/petner/anidoc/domain/vet/reservation/dto/ReservationResponseDto.java index 2b08da8..8b6bef6 100644 --- a/backend/src/main/java/com/petner/anidoc/domain/vet/reservation/dto/ReservationResponseDto.java +++ b/backend/src/main/java/com/petner/anidoc/domain/vet/reservation/dto/ReservationResponseDto.java @@ -22,6 +22,7 @@ public class ReservationResponseDto { private String userName; private Long petId; private String petName; + private String petSpecies; private Long doctorId; private String doctorName; private LocalDate reservationDate; @@ -51,6 +52,7 @@ public static ReservationResponseDto fromEntity(Reservation reservation) { .userName(reservation.getUser().getName()) .petId(reservation.getPet().getId()) .petName(reservation.getPet().getName()) + .petSpecies(reservation.getPet().getSpecies()) .doctorId(doctorId) .doctorName(doctorName) .reservationDate(reservation.getReservationDate()) @@ -80,6 +82,7 @@ public static ReservationResponseDto fromEntity(Reservation reservation, boolean .userName(reservation.getUser().getName()) .petId(reservation.getPet().getId()) .petName(reservation.getPet().getName()) + .petSpecies(reservation.getPet().getSpecies()) .doctorId(doctorId) .doctorName(doctorName) .reservationDate(reservation.getReservationDate()) diff --git a/backend/src/main/java/com/petner/anidoc/domain/vet/vaccination/controller/DoctorPetVaccineController.java b/backend/src/main/java/com/petner/anidoc/domain/vet/vaccination/controller/DoctorPetVaccineController.java index bf617e6..611d57d 100644 --- a/backend/src/main/java/com/petner/anidoc/domain/vet/vaccination/controller/DoctorPetVaccineController.java +++ b/backend/src/main/java/com/petner/anidoc/domain/vet/vaccination/controller/DoctorPetVaccineController.java @@ -4,6 +4,7 @@ import com.petner.anidoc.domain.user.user.repository.UserRepository; import com.petner.anidoc.domain.vet.vaccination.dto.DoctorPetVaccineRequestDTO; import com.petner.anidoc.domain.vet.vaccination.dto.DoctorPetVaccineResponseDTO; +import com.petner.anidoc.domain.vet.vaccination.dto.VaccinationStatusDto; import com.petner.anidoc.domain.vet.vaccination.entity.Vaccination; import com.petner.anidoc.domain.vet.vaccination.service.DoctorPetVaccineService; import io.swagger.v3.oas.annotations.Operation; @@ -65,11 +66,11 @@ public ResponseEntity updatePetVaccine( .collect(Collectors.joining(", ")); return ResponseEntity.badRequest().body(errorMsg); } - User user = userRepository.findByEmail(currentUser.getUsername()) + User currentDoctor = userRepository.findByEmail(currentUser.getUsername()) .orElseThrow(() -> new RuntimeException("사용자 정보를 찾을 수 없습니다.")); - DoctorPetVaccineResponseDTO doctorPetVaccinResponseDTO = doctorPetVaccineService.updateVaccine(vaccinationId, doctorPetVaccinRequestDTO); - return ResponseEntity.ok(doctorPetVaccinResponseDTO); + DoctorPetVaccineResponseDTO doctorPetVaccineResponseDTO = doctorPetVaccineService.updateVaccine(vaccinationId, doctorPetVaccinRequestDTO, currentDoctor); + return ResponseEntity.ok(doctorPetVaccineResponseDTO); } //전체조회 @@ -88,12 +89,32 @@ public ResponseEntity getVaccinationDetail(@PathVar return ResponseEntity.ok(result); } + @GetMapping("/reservation/{reservationId}") + @Operation(summary = "예약별 예방접종 기록 조회", description = "특정 예약에 대한 예방접종 기록을 조회합니다") + public ResponseEntity getVaccinationByReservation( + @PathVariable Long reservationId) { + DoctorPetVaccineResponseDTO result = doctorPetVaccineService.findVaccinationByReservationId(reservationId); + return ResponseEntity.ok(result); + } + //삭제 @DeleteMapping("/{vaccinationId}") @Operation(summary = "예방접종 삭제", description = "예방접종 삭제") - public ResponseEntity deleteVaccination(@PathVariable Long vaccinationId) { - doctorPetVaccineService.deleteVaccination(vaccinationId); + public ResponseEntity deleteVaccination( + @PathVariable Long vaccinationId, + @AuthenticationPrincipal UserDetails currentUser + ) { + User currentDoctor = userRepository.findByEmail(currentUser.getUsername()) + .orElseThrow(()-> new RuntimeException("사용자 정보를 찾을 수 없습니다.")); + + doctorPetVaccineService.deleteVaccination(vaccinationId, currentDoctor); return ResponseEntity.ok().body("예방접종 기록이 삭제되었습니다."); } + @GetMapping("/status/reservation/{reservationId}") + @Operation(summary = "예약별 백신 기록 상태 확인", description = "특정 예약에 백신 기록 상태를 확인합니다") + public ResponseEntity getVaccinationStatus(@PathVariable Long reservationId) { + VaccinationStatusDto status = doctorPetVaccineService.getVaccinationStatusByReservationId(reservationId); + return ResponseEntity.ok(status); + } } diff --git a/backend/src/main/java/com/petner/anidoc/domain/vet/vaccination/dto/DoctorPetVaccineRequestDTO.java b/backend/src/main/java/com/petner/anidoc/domain/vet/vaccination/dto/DoctorPetVaccineRequestDTO.java index eed2bba..c9f7d16 100644 --- a/backend/src/main/java/com/petner/anidoc/domain/vet/vaccination/dto/DoctorPetVaccineRequestDTO.java +++ b/backend/src/main/java/com/petner/anidoc/domain/vet/vaccination/dto/DoctorPetVaccineRequestDTO.java @@ -37,7 +37,6 @@ public class DoctorPetVaccineRequestDTO { @DateTimeFormat(pattern = "yyyy-MM-dd") private LocalDate nextDueDate; //다음접종일 - @NotNull private VaccinationStatus status; //접종상태(미접종, 접종진행중(2차까지맞고, 3차가 남은경우), 모든접종완료) private String notes; //메모 diff --git a/backend/src/main/java/com/petner/anidoc/domain/vet/vaccination/dto/VaccinationStatusDto.java b/backend/src/main/java/com/petner/anidoc/domain/vet/vaccination/dto/VaccinationStatusDto.java new file mode 100644 index 0000000..c9bb9eb --- /dev/null +++ b/backend/src/main/java/com/petner/anidoc/domain/vet/vaccination/dto/VaccinationStatusDto.java @@ -0,0 +1,15 @@ +package com.petner.anidoc.domain.vet.vaccination.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class VaccinationStatusDto { + private boolean exists; + private String status; +} \ No newline at end of file diff --git a/backend/src/main/java/com/petner/anidoc/domain/vet/vaccination/service/DoctorPetVaccineService.java b/backend/src/main/java/com/petner/anidoc/domain/vet/vaccination/service/DoctorPetVaccineService.java index 4f30d26..b998cde 100644 --- a/backend/src/main/java/com/petner/anidoc/domain/vet/vaccination/service/DoctorPetVaccineService.java +++ b/backend/src/main/java/com/petner/anidoc/domain/vet/vaccination/service/DoctorPetVaccineService.java @@ -8,14 +8,18 @@ import com.petner.anidoc.domain.vet.reservation.repository.ReservationRepository; import com.petner.anidoc.domain.vet.vaccination.dto.DoctorPetVaccineRequestDTO; import com.petner.anidoc.domain.vet.vaccination.dto.DoctorPetVaccineResponseDTO; +import com.petner.anidoc.domain.vet.vaccination.dto.VaccinationStatusDto; import com.petner.anidoc.domain.vet.vaccination.entity.Vaccination; +import com.petner.anidoc.domain.vet.vaccination.entity.VaccinationStatus; import com.petner.anidoc.domain.vet.vaccination.repository.VaccinationRepository; import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; +import org.springframework.security.access.AccessDeniedException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; @Service @@ -41,6 +45,10 @@ public Vaccination registerVaccination(Long petId, DoctorPetVaccineRequestDTO do throw new IllegalArgumentException("해당 예약은 이 반려동물의 예약이 아닙니다."); } + if (vaccinationRepository.findByReservationId(doctorPetVaccineRequestDTO.getReservationId()).isPresent()) { + throw new IllegalStateException("이미 해당 예약에 대한 예방접종 기록이 존재합니다."); + } + Vaccination vaccination = Vaccination.builder() .doctor(doctor) .pet(pet) @@ -48,9 +56,9 @@ public Vaccination registerVaccination(Long petId, DoctorPetVaccineRequestDTO do .vaccineName(doctorPetVaccineRequestDTO.getVaccineName()) .currentDose(doctorPetVaccineRequestDTO.getCurrentDose()) .totalDoses(doctorPetVaccineRequestDTO.getTotalDoses()) - .vaccinationDate(doctorPetVaccineRequestDTO.getVaccinationDate()) //추가 - .nextDueDate(doctorPetVaccineRequestDTO.getNextDueDate()) - .status(doctorPetVaccineRequestDTO.getStatus()) + .vaccinationDate(reservation.getReservationDate()) // 예약일로 자동 설정 + .nextDueDate(null) // 다음 접종일은 설정하지 않음 + .status(VaccinationStatus.NOT_STARTED) .notes(doctorPetVaccineRequestDTO.getNotes()) .build(); @@ -58,9 +66,13 @@ public Vaccination registerVaccination(Long petId, DoctorPetVaccineRequestDTO do } //수정 @Transactional - public DoctorPetVaccineResponseDTO updateVaccine(Long vaccinationId, DoctorPetVaccineRequestDTO doctorPetVaccineRequestDTO){ + public DoctorPetVaccineResponseDTO updateVaccine(Long vaccinationId, DoctorPetVaccineRequestDTO doctorPetVaccineRequestDTO, User currentDoctor){ Vaccination vaccination = vaccinationRepository.findById(vaccinationId) .orElseThrow(()-> new RuntimeException("예방접종 기록이 없습니다.")); + //권한(동일한 의료진인지 확인) + if (!vaccination.getDoctor().getId().equals(currentDoctor.getId())) { + throw new AccessDeniedException("본인이 등록한 예방접종만 수정할 수 있습니다."); + } User doctor = userRepository.findById(doctorPetVaccineRequestDTO.getDoctorId()) .orElseThrow(() -> new RuntimeException("의사 정보가 없습니다.")); Reservation reservation = reservationRepository.findById(doctorPetVaccineRequestDTO.getReservationId()) @@ -92,12 +104,32 @@ public DoctorPetVaccineResponseDTO findVaccinationById(Long vaccinationId) { .orElseThrow(() -> new RuntimeException("예방접종 기록이 없습니다.")); return new DoctorPetVaccineResponseDTO(vaccination); } + + // 예약별 예방접종 기록 조회 + @Transactional(readOnly = true) + public DoctorPetVaccineResponseDTO findVaccinationByReservationId(Long reservationId) { + Vaccination vaccination = vaccinationRepository.findByReservationId(reservationId) + .orElseThrow(() -> new EntityNotFoundException("해당 예약에 대한 예방접종 기록이 없습니다.")); + return new DoctorPetVaccineResponseDTO(vaccination); + } + //삭제 @Transactional - public void deleteVaccination(Long vaccinationId) { + public void deleteVaccination(Long vaccinationId, User currentDoctor) { Vaccination vaccination = vaccinationRepository.findById(vaccinationId) .orElseThrow(() -> new RuntimeException("예방접종 기록이 없습니다.")); + //권한(동일한 의료진인지 확인) + if (!vaccination.getDoctor().getId().equals(currentDoctor.getId())){ + throw new AccessDeniedException("본인이 등록한 예방접종만 삭제할 수 있습니다."); + } + vaccinationRepository.delete(vaccination); } + @Transactional(readOnly = true) + public VaccinationStatusDto getVaccinationStatusByReservationId(Long reservationId) { + Optional vaccination = vaccinationRepository.findByReservationId(reservationId); + return vaccination.map(value -> new VaccinationStatusDto(true, value.getStatus().toString())) + .orElseGet(() -> new VaccinationStatusDto(false, null)); + } } diff --git a/frontend/src/app/medical-records/page.tsx b/frontend/src/app/medical-records/page.tsx index 7aa4485..664f4fa 100644 --- a/frontend/src/app/medical-records/page.tsx +++ b/frontend/src/app/medical-records/page.tsx @@ -14,6 +14,7 @@ interface MedicalRecord { id: number; reservationTime: string; petName: string; + petSpecies: string; symptom: string; doctorName: string; status: string; @@ -52,7 +53,9 @@ interface MedicalRecord { reservationDate?: string; createdAt?: string; updatedAt?: string; - type?: string; + type?: "GENERAL" | "VACCINATION"; + hasVaccinationRecord?: boolean; // 예방접종 기록 존재 여부 + vaccinationStatus?: string; } interface Stats { @@ -155,7 +158,38 @@ export default function MedicalRecordPage() { }) ); - setRecords(recordsWithDetails); + if (user.userRole === "ROLE_STAFF") { + const recordsWithVaccinationStatus = await Promise.all( + recordsWithDetails.map(async (record) => { + if (record.type === "VACCINATION") { + try { + const vaccinationRes = await fetch( + `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/doctor/vaccines/status/reservation/${record.id}`, + { credentials: "include" } + ); + if (vaccinationRes.ok) { + const statusData = await vaccinationRes.json(); + return { + ...record, + hasVaccinationRecord: statusData.exists, + vaccinationStatus: statusData.status || undefined, + }; + } + } catch (err) { + console.error("백신 기록 확인 실패:", err); + } + } + return { + ...record, + hasVaccinationRecord: false, + vaccinationStatus: undefined, + }; + }) + ); + setRecords(recordsWithVaccinationStatus); + } else { + setRecords(recordsWithDetails); + } } catch (err) { console.error(err); } finally { diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index cd9ebfd..e535546 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -111,20 +111,7 @@ export default function Sidebar() { 진료 기록 -
  • - {(user.userRole === "ROLE_STAFF" || - user.userRole === "ROLE_ADMIN") && ( - - - 접종 관리 - - )} -
  • + {user.userRole === "ROLE_ADMIN" && (
  • (null); const [currentPage, setCurrentPage] = useState(1); const recordsPerPage = 10; @@ -81,47 +90,91 @@ export default function StaffMedicalRecord({ const [showEditModal, setShowEditModal] = useState(false); const handleClick = async (record: MedicalRecord) => { - if (record.hasMedicalRecord) { - try { - const res = await fetch( - `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/medical-records/by-reservation/${record.id}?userId=${record.userId}`, - { - credentials: "include", - } - ); - const responseBody = await res.json(); - const medicalRecord = responseBody.medicalRecord; + if (record.type === "VACCINATION") { + // 예방접종 기록 처리 + if (record.hasVaccinationRecord) { + // 예방접종 기록 조회 + await handleViewVaccinationRecord(record); + } else { + // 예방접종 기록 작성 + setSelectedRecord(record); + setShowVaccinationModal(true); + } + } else { + // 일반 진료 기록 처리 + if (record.hasMedicalRecord) { + // 진료 기록 조회 + await handleViewMedicalRecord(record); + } else { + // 진료 기록 작성 + setSelectedRecord(record); + onOpenChart(record); + } + } + }; - if (!medicalRecord) { - alert("진료기록을 찾을 수 없습니다."); - return; + const handleViewMedicalRecord = async (record: MedicalRecord) => { + try { + const res = await fetch( + `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/medical-records/by-reservation/${record.id}?userId=${record.userId}`, + { + credentials: "include", } + ); + const responseBody = await res.json(); + const medicalRecord = responseBody.medicalRecord; - const updatedRecord = { - ...record, - id: medicalRecord.id, - reservationId: record.id, - weight: medicalRecord.currentWeight, - age: medicalRecord.age, - diagnosis: medicalRecord.diagnosis, - treatment: medicalRecord.treatment, - surgery: medicalRecord.surgery, - hospitalization: medicalRecord.hospitalization, - checkups: medicalRecord.checkups, - hasMedicalRecord: true, - petId: medicalRecord.petId ?? record.petId, - }; - - setSelectedRecord(updatedRecord); - setShowDetail(true); - } catch (err) { - console.error("진료기록 조회 실패", err); - alert("진료기록을 불러오는 데 실패했습니다."); + if (!medicalRecord) { + alert("진료기록을 찾을 수 없습니다."); + return; } - } else { - // 진료기록이 없을 때는 그대로 예약 데이터 전달 + + const updatedRecord = { + ...record, + id: medicalRecord.id, + reservationId: record.id, + weight: medicalRecord.currentWeight, + age: medicalRecord.age, + diagnosis: medicalRecord.diagnosis, + treatment: medicalRecord.treatment, + surgery: medicalRecord.surgery, + hospitalization: medicalRecord.hospitalization, + checkups: medicalRecord.checkups, + hasMedicalRecord: true, + petId: medicalRecord.petId ?? record.petId, + }; + + setSelectedRecord(updatedRecord); + setShowDetail(true); + } catch (err) { + console.error("진료기록 조회 실패", err); + alert("진료기록을 불러오는 데 실패했습니다."); + } + }; + + const handleViewVaccinationRecord = async (record: MedicalRecord) => { + try { + // 예방접종 기록 조회 API 호출 + const res = await fetch( + `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/doctor/vaccines/reservation/${record.id}`, + { + credentials: "include", + } + ); + + if (!res.ok) { + throw new Error("예방접종 기록을 찾을 수 없습니다."); + } + + const vaccinationData = await res.json(); + + // 예방접종 기록 상세 모달 표시 (별도 모달 사용) setSelectedRecord(record); - onOpenChart(record); + setSelectedVaccinationData(vaccinationData); + setShowVaccinationDetail(true); + } catch (err) { + console.error("예방접종 기록 조회 실패", err); + alert("예방접종 기록을 불러오는 데 실패했습니다."); } }; @@ -204,10 +257,96 @@ export default function StaffMedicalRecord({ } catch (err) {} }; + const handleVaccinationSaved = async () => { + // 예방접종 기록 저장 후 처리 + if (selectedRecord) { + const updatedRecord = { + ...selectedRecord, + hasVaccinationRecord: true, + }; + setSelectedRecord(updatedRecord); + onOpenChart(updatedRecord); + } + setShowVaccinationModal(false); + }; + const handleCloseModal = () => { setSelectedRecord(null); setShowDetail(false); - setChartModalOpen(false); // ChartModal 명시적으로 닫음 + setChartModalOpen(false); + setShowVaccinationModal(false); + setShowVaccinationDetail(false); + setSelectedVaccinationData(null); + }; + + const getVaccinationStatusText = (status: string) => { + switch (status) { + case "NOT_STARTED": + return "미접종"; + case "IN_PROGRESS": + return "접종진행중"; + case "COMPLETED": + return "접종완료"; + default: + return "진료전"; + } + }; + + const getVaccinationStatusColor = (status: string) => { + switch (status) { + case "NOT_STARTED": + return "bg-red-100 text-red-800"; + case "IN_PROGRESS": + return "bg-yellow-100 text-yellow-800"; + case "COMPLETED": + return "bg-green-100 text-green-800"; + default: + return "bg-yellow-100 text-yellow-800"; + } + }; + + const getActionButton = (record: MedicalRecord) => { + if (record.type === "VACCINATION") { + if (record.hasVaccinationRecord) { + return ( + + ); + } else { + return ( + + ); + } + } else { + if (record.hasMedicalRecord) { + return ( + + ); + } else { + return ( + + ); + } + } }; return ( @@ -243,6 +382,9 @@ export default function StaffMedicalRecord({ 환자명 + + 진료 유형 + 증상 @@ -260,13 +402,13 @@ export default function StaffMedicalRecord({ {loading ? ( - + 로딩 중... ) : records.length === 0 ? ( - + 기록이 없습니다. @@ -301,6 +443,17 @@ export default function StaffMedicalRecord({ {r.petName} + + + {r.type === "GENERAL" ? "일반진료" : "예방접종"} + + {r.symptom} @@ -310,34 +463,28 @@ export default function StaffMedicalRecord({ - {r.hasMedicalRecord ? "진료완료" : "진료전"} + {r.type === "VACCINATION" && + r.hasVaccinationRecord && + r.vaccinationStatus + ? getVaccinationStatusText(r.vaccinationStatus) // 예방접종 상태 텍스트 + : (r.type === "GENERAL" && r.hasMedicalRecord) || + (r.type === "VACCINATION" && r.hasVaccinationRecord) + ? "진료완료" + : "진료전"} - - - + {getActionButton(r)} )) )} @@ -465,6 +612,29 @@ export default function StaffMedicalRecord({ onSaved={(id, reservationId) => handleSaved(id, reservationId)} /> )} + + {showVaccinationModal && selectedRecord && ( + + )} + + {showVaccinationDetail && selectedVaccinationData && ( + { + setSelectedVaccinationData(updatedData); + }} + /> + )} ); } diff --git a/frontend/src/components/medical-records/VaccinationDetailModal.tsx b/frontend/src/components/medical-records/VaccinationDetailModal.tsx new file mode 100644 index 0000000..22103b8 --- /dev/null +++ b/frontend/src/components/medical-records/VaccinationDetailModal.tsx @@ -0,0 +1,370 @@ +"use client"; + +import { X, Edit, Save } from "lucide-react"; +import { useState } from "react"; + +interface VaccinationData { + id: number; + petName: string; + doctorName: string; + vaccineName: string; + currentDose: number; + totalDoses: number; + vaccinationDate: string; + nextDueDate?: string; + status: "NOT_STARTED" | "IN_PROGRESS" | "COMPLETED"; + notes?: string; +} + +interface ReservationData { + reservationId?: number; + doctorId?: number; +} + +interface VaccinationDetailModalProps { + onClose: () => void; + vaccinationData: VaccinationData; + reservationData: ReservationData; + onEdit?: () => void; + userRole?: string; + onUpdate?: (updatedData: VaccinationData) => void; +} + +export default function VaccinationDetailModal({ + onClose, + vaccinationData, + reservationData, + onEdit, + userRole, + onUpdate, +}: VaccinationDetailModalProps) { + const [isEditing, setIsEditing] = useState(false); + const [editData, setEditData] = useState(vaccinationData); + const [loading, setLoading] = useState(false); + + const getVaccineNameInKorean = (englishName: string) => { + const vaccineMap: { [key: string]: string } = { + DHPPL: "종합백신(DHPPL)", + CORONA: "코로나 장염", + KENNEL_COUGH: "켄넬코프", + INFLUENZA: "인플루엔자", + RABIES: "광견병", + ANTIBODY_TEST: "항체가검사", + HEARTWORM: "심장사상충", + FVRCP: "종합백신(FVRCP)", + FIP: "전염성 복막염", + }; + return vaccineMap[englishName] || englishName; + }; + + const getStatusText = (status: string) => { + switch (status) { + case "NOT_STARTED": + return "미접종"; + case "IN_PROGRESS": + return "접종진행중"; + case "COMPLETED": + return "접종완료"; + default: + return "-"; + } + }; + + const getStatusColor = (status: string) => { + switch (status) { + case "IN_PROGRESS": + return "bg-yellow-100 text-yellow-800"; + case "COMPLETED": + return "bg-green-100 text-green-800"; + default: + return "bg-gray-100 text-gray-800"; + } + }; + + const formatDate = (dateString?: string) => { + if (!dateString) return "-"; + + try { + const date = new Date(dateString); + return date.toLocaleDateString("ko-KR", { + year: "numeric", + month: "long", + day: "numeric", + }); + } catch (error) { + return dateString; + } + }; + + const handleSave = async () => { + setLoading(true); + try { + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/doctor/vaccines/${vaccinationData.id}`, + { + method: "PUT", + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + doctorId: reservationData.doctorId, + reservationId: reservationData.reservationId, + vaccineName: editData.vaccineName, + currentDose: editData.currentDose, + totalDoses: editData.totalDoses, + vaccinationDate: editData.vaccinationDate, + nextDueDate: null, + status: editData.status, + notes: editData.notes, + }), + } + ); + + if (!response.ok) { + throw new Error("백신 기록 수정에 실패했습니다"); + } + + const updatedVaccinationData = { + ...vaccinationData, + ...editData, + }; + + // 상태 업데이트 + setIsEditing(false); + if (onUpdate) { + onUpdate(updatedVaccinationData); + } + + alert("백신 기록이 성공적으로 수정되었습니다."); + } catch (error) { + console.error("백신 기록 수정 오류:", error); + alert("백신 기록 수정에 실패했습니다."); + } finally { + setLoading(false); + } + }; + + return ( +
    +
    + + +

    + 예방접종 기록 상세 +

    + + {/* 기본 정보 (항상 읽기 전용) */} +
    +
    + +
    + {vaccinationData.petName} +
    +
    +
    + +
    + {vaccinationData.doctorName} +
    +
    +
    + + {/* 백신 정보 */} +
    +
    + + {isEditing ? ( + + ) : ( +
    + {getVaccineNameInKorean(vaccinationData.vaccineName)} +
    + )} +
    +
    + + {isEditing ? ( + + ) : ( +
    + + {getStatusText(vaccinationData.status)} + +
    + )} +
    +
    + + {/* 접종 회차 */} +
    +
    + + {isEditing ? ( + + setEditData({ + ...editData, + currentDose: parseInt(e.target.value) || 1, + }) + } + className="w-full border border-gray-300 rounded px-3 py-2 focus:border-teal-500 focus:ring-teal-500" + disabled={loading} + /> + ) : ( +
    + {vaccinationData.currentDose}회차 +
    + )} +
    +
    + + {isEditing ? ( + + setEditData({ + ...editData, + totalDoses: parseInt(e.target.value) || 1, + }) + } + className="w-full border border-gray-300 rounded px-3 py-2 focus:border-teal-500 focus:ring-teal-500" + disabled={loading} + /> + ) : ( +
    + {vaccinationData.totalDoses}회차 +
    + )} +
    +
    + + {/* 접종일 */} +
    + +
    + {formatDate(vaccinationData.vaccinationDate)} +
    +
    + + {/* 메모 */} +
    + + {isEditing ? ( +