diff --git a/.claude/rules/git.md b/.claude/rules/git.md new file mode 100644 index 0000000..d14cec0 --- /dev/null +++ b/.claude/rules/git.md @@ -0,0 +1,21 @@ +# Git 워크플로우 규칙 + +## 커밋 타이밍 +- 커밋/푸시는 사용자가 명시적으로 요청할 때만 수행한다 +- 파일 수정 후 임의로 커밋/푸시하지 않는다 + +## 커밋 분리 원칙 +한 번에 전부 커밋하지 않는다. 아래 순서로 레이어별 분리: + +1. `entity` / `enums` 변경 시 +2. `dto` (request / response) +3. `converter` +4. `repository` +5. `service` (command / query 각각 별도) +6. `controller` +7. `config` (SecurityConfig 등) + +## 커밋 메시지 +- 본문은 한글로 작성한다 +- 기술 용어(DTO, OCR, ES, JWT 등)는 영어 그대로 쓴다 — 괄호로 번역 추가하지 않는다 +- 접두사: `feat:`, `fix:`, `chore:`, `refactor:`, `docs:` diff --git a/CLAUDE.md b/CLAUDE.md index 73f0ea7..a16e41e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,6 +35,7 @@ Don't assume. Don't hide confusion. Surface tradeoffs. - 테스트 작성/수정 시 → `testing_guide.md` - Security/Config 만질 때 → `security.md` - 배포/Docker/GitHub Actions 관련 → `deploy.md` +- 커밋/푸시 작업 시 → `git.md` (항상 로드) ### 🟣 AI 작업 흔적 (`.dev/`) - 새로 알게 된 패턴·주의점·오류 기록 → `learnings/` diff --git a/src/main/java/com/piuda/callcare/domain/druginfo/controller/DrugInfoController.java b/src/main/java/com/piuda/callcare/domain/druginfo/controller/DrugInfoController.java index b6a70a5..274051f 100644 --- a/src/main/java/com/piuda/callcare/domain/druginfo/controller/DrugInfoController.java +++ b/src/main/java/com/piuda/callcare/domain/druginfo/controller/DrugInfoController.java @@ -18,7 +18,7 @@ import java.util.List; -@Tag(name = "Drug", description = "약품 정보 API (DB 캐시 기반)") +@Tag(name = "Drug", description = "약품 정보 API (MySQL DB 기반 — ES 장애 시 폴백용)") @Validated @RestController @RequestMapping("/api/drugs") diff --git a/src/main/java/com/piuda/callcare/domain/medication/controller/MedicationController.java b/src/main/java/com/piuda/callcare/domain/medication/controller/MedicationController.java index 00baacc..422a668 100644 --- a/src/main/java/com/piuda/callcare/domain/medication/controller/MedicationController.java +++ b/src/main/java/com/piuda/callcare/domain/medication/controller/MedicationController.java @@ -1,8 +1,10 @@ package com.piuda.callcare.domain.medication.controller; import com.piuda.callcare.domain.medication.dto.request.MedicationCreateRequest; +import com.piuda.callcare.domain.medication.dto.response.MedicationDetailResponse; import com.piuda.callcare.domain.medication.dto.response.MedicationResponse; import com.piuda.callcare.domain.medication.service.command.MedicationCommandService; +import com.piuda.callcare.domain.medication.service.query.MedicationQueryService; import com.piuda.callcare.global.common.response.ApiResponse; import com.piuda.callcare.global.common.response.ResponseUtils; import io.swagger.v3.oas.annotations.Operation; @@ -22,6 +24,7 @@ public class MedicationController { private final MedicationCommandService medicationCommandService; + private final MedicationQueryService medicationQueryService; @Operation(summary = "약 등록", description = "약을 1개 이상 등록합니다. 단건이면 리스트에 1개, 여러 개면 여러 개 담아서 보내세요. 하나라도 실패하면 전체 롤백됩니다.") @PostMapping("/batch") @@ -31,4 +34,13 @@ public ResponseEntity>> registerBatch( ) { return ResponseUtils.created(medicationCommandService.registerBatch(userId, requests)); } + + @Operation(summary = "약 상세 정보 조회", description = "약 카드 클릭 시 주의사항/부작용/용법·용량/효능·효과를 반환합니다. DrugInfo 미연결 약은 의약품 정보 필드 null로 반환됩니다.") + @GetMapping("/{medicationId}/detail") + public ResponseEntity> getDetail( + @AuthenticationPrincipal Long userId, + @PathVariable Long medicationId + ) { + return ResponseUtils.ok(medicationQueryService.getDetail(userId, medicationId)); + } } diff --git a/src/main/java/com/piuda/callcare/domain/medication/converter/MedicationConverter.java b/src/main/java/com/piuda/callcare/domain/medication/converter/MedicationConverter.java index 0ad4f85..7b70b87 100644 --- a/src/main/java/com/piuda/callcare/domain/medication/converter/MedicationConverter.java +++ b/src/main/java/com/piuda/callcare/domain/medication/converter/MedicationConverter.java @@ -1,5 +1,7 @@ package com.piuda.callcare.domain.medication.converter; +import com.piuda.callcare.domain.druginfo.entity.DrugInfo; +import com.piuda.callcare.domain.medication.dto.response.MedicationDetailResponse; import com.piuda.callcare.domain.medication.dto.response.MedicationResponse; import com.piuda.callcare.domain.medication.entity.Medication; import com.piuda.callcare.domain.medication.entity.MedicationSchedule; @@ -11,6 +13,21 @@ @Component public class MedicationConverter { + // Medication → MedicationDetailResponse (약 상세 조회용, DrugInfo 없으면 전 필드 null) + public MedicationDetailResponse toDetailResponse(Medication medication) { + DrugInfo drugInfo = medication.getDrugInfo(); + if (drugInfo == null) { + return new MedicationDetailResponse(medication.getDrugName(), null, null, null, null); + } + return new MedicationDetailResponse( + medication.getDrugName(), + drugInfo.getEfcyQesitm(), + drugInfo.getUseMethodQesitm(), + drugInfo.getAtpnQesitm(), + drugInfo.getSeQesitm() + ); + } + // Medication + 스케줄 목록 → MedicationResponse public MedicationResponse toResponse(Medication medication, List schedules) { List mealTimes = schedules.stream() diff --git a/src/main/java/com/piuda/callcare/domain/medication/dto/request/MedicationCreateRequest.java b/src/main/java/com/piuda/callcare/domain/medication/dto/request/MedicationCreateRequest.java index 9a7dc23..d6c55d9 100644 --- a/src/main/java/com/piuda/callcare/domain/medication/dto/request/MedicationCreateRequest.java +++ b/src/main/java/com/piuda/callcare/domain/medication/dto/request/MedicationCreateRequest.java @@ -38,5 +38,8 @@ public record MedicationCreateRequest( LocalDate prescriptionDate, @Schema(description = "병원 이름 — 직접 입력, 없으면 null") - String hospitalName + String hospitalName, + + @Schema(description = "메모 (복용방법 + 보관방법 자동 생성 또는 사용자 직접 입력, 없으면 null)") + String memo ) {} diff --git a/src/main/java/com/piuda/callcare/domain/medication/dto/response/MedicationDetailResponse.java b/src/main/java/com/piuda/callcare/domain/medication/dto/response/MedicationDetailResponse.java new file mode 100644 index 0000000..9caeb76 --- /dev/null +++ b/src/main/java/com/piuda/callcare/domain/medication/dto/response/MedicationDetailResponse.java @@ -0,0 +1,23 @@ +package com.piuda.callcare.domain.medication.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "약 상세 정보 응답 — drugName은 항상 반환, DrugInfo 미연결 시 의약품 정보 필드 null") +public record MedicationDetailResponse( + + @Schema(description = "약품명") + String drugName, + + @Schema(description = "효능·효과 (없으면 null)") + String efcyQesitm, + + @Schema(description = "용법·용량 (없으면 null)") + String useMethodQesitm, + + @Schema(description = "주의사항 (없으면 null)") + String atpnQesitm, + + @Schema(description = "부작용 (없으면 null)") + String seQesitm +) { +} diff --git a/src/main/java/com/piuda/callcare/domain/medication/service/command/MedicationCommandService.java b/src/main/java/com/piuda/callcare/domain/medication/service/command/MedicationCommandService.java index e9184fa..2f15ba7 100644 --- a/src/main/java/com/piuda/callcare/domain/medication/service/command/MedicationCommandService.java +++ b/src/main/java/com/piuda/callcare/domain/medication/service/command/MedicationCommandService.java @@ -66,6 +66,7 @@ private MedicationResponse doRegister(Long userId, MedicationCreateRequest reque .prescriptionDate(request.prescriptionDate()) .isActive(true) .ocrResultId(request.ocrResultId()) + .memo(request.memo()) .build(); Medication saved = medicationRepository.save(medication); diff --git a/src/main/java/com/piuda/callcare/domain/medication/service/query/MedicationQueryService.java b/src/main/java/com/piuda/callcare/domain/medication/service/query/MedicationQueryService.java new file mode 100644 index 0000000..9ed22d0 --- /dev/null +++ b/src/main/java/com/piuda/callcare/domain/medication/service/query/MedicationQueryService.java @@ -0,0 +1,29 @@ +package com.piuda.callcare.domain.medication.service.query; + +import com.piuda.callcare.domain.medication.converter.MedicationConverter; +import com.piuda.callcare.domain.medication.dto.response.MedicationDetailResponse; +import com.piuda.callcare.domain.medication.entity.Medication; +import com.piuda.callcare.domain.medication.repository.MedicationRepository; +import com.piuda.callcare.global.exception.CallCareException; +import com.piuda.callcare.global.exception.ErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class MedicationQueryService { + + private final MedicationRepository medicationRepository; + private final MedicationConverter medicationConverter; + + public MedicationDetailResponse getDetail(Long userId, Long medicationId) { + Medication medication = medicationRepository.findById(medicationId) + .orElseThrow(() -> new CallCareException(ErrorCode.MEDICATION_NOT_FOUND)); + if (userId != null && !medication.getSenior().getUser().getId().equals(userId)) { + throw new CallCareException(ErrorCode.FORBIDDEN); + } + return medicationConverter.toDetailResponse(medication); + } +} diff --git a/src/main/java/com/piuda/callcare/global/config/SecurityConfig.java b/src/main/java/com/piuda/callcare/global/config/SecurityConfig.java index 6e823ca..f136fc9 100644 --- a/src/main/java/com/piuda/callcare/global/config/SecurityConfig.java +++ b/src/main/java/com/piuda/callcare/global/config/SecurityConfig.java @@ -43,7 +43,9 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers(HttpMethod.GET, "/api/conflicts/**").permitAll() .requestMatchers(HttpMethod.POST, "/api/conflicts/**").permitAll() .requestMatchers(HttpMethod.POST, "/api/ocr/**").permitAll() + // TODO: 인증 필터 도입 후 제거하고, userId 기반 소유권 검증으로 전환 .requestMatchers(HttpMethod.POST, "/api/medications/batch").permitAll() + .requestMatchers(HttpMethod.GET, "/api/medications/**").permitAll() .anyRequest().authenticated() ) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);