From 8bb709715615b28a689cc5e04ce7013a0b093386 Mon Sep 17 00:00:00 2001 From: kangcheolung Date: Wed, 8 Jul 2026 19:12:47 +0900 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20=EC=95=BD=20=EB=A9=94=EB=AA=A8=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=95=BD?= =?UTF-8?q?=20=EC=83=81=EC=84=B8=20=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MedicationCreateRequest에 memo 필드 추가 (복용방법 + 보관방법) - MedicationCommandService에서 memo 저장 처리 - MedicationQueryService 신규 생성 — GET /api/medications/{id}/detail - MedicationDetailResponse 신규 생성 (efcyQesitm, useMethodQesitm, atpnQesitm, seQesitm) - MedicationConverter에 toDetailResponse 변환 메서드 추가 - SecurityConfig에 GET /api/medications/** permitAll 추가 - DrugInfoController Swagger 설명 보완 Co-Authored-By: Claude Sonnet 4.6 --- .../controller/DrugInfoController.java | 2 +- .../controller/MedicationController.java | 11 ++++++++ .../converter/MedicationConverter.java | 17 ++++++++++++ .../dto/request/MedicationCreateRequest.java | 5 +++- .../response/MedicationDetailResponse.java | 23 ++++++++++++++++ .../command/MedicationCommandService.java | 1 + .../service/query/MedicationQueryService.java | 26 +++++++++++++++++++ .../global/config/SecurityConfig.java | 1 + 8 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/piuda/callcare/domain/medication/dto/response/MedicationDetailResponse.java create mode 100644 src/main/java/com/piuda/callcare/domain/medication/service/query/MedicationQueryService.java 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..2d6e928 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,12 @@ public ResponseEntity>> registerBatch( ) { return ResponseUtils.created(medicationCommandService.registerBatch(userId, requests)); } + + @Operation(summary = "약 상세 정보 조회", description = "약 카드 클릭 시 주의사항/부작용/용법·용량/효능·효과를 반환합니다. DrugInfo 미연결 약은 전 필드 null로 반환됩니다.") + @GetMapping("/{medicationId}/detail") + public ResponseEntity> getDetail( + @PathVariable Long medicationId + ) { + return ResponseUtils.ok(medicationQueryService.getDetail(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..ac13e2d --- /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 = "약 상세 정보 응답 — 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..846a9f7 --- /dev/null +++ b/src/main/java/com/piuda/callcare/domain/medication/service/query/MedicationQueryService.java @@ -0,0 +1,26 @@ +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 medicationId) { + Medication medication = medicationRepository.findById(medicationId) + .orElseThrow(() -> new CallCareException(ErrorCode.MEDICATION_NOT_FOUND)); + 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..2e29278 100644 --- a/src/main/java/com/piuda/callcare/global/config/SecurityConfig.java +++ b/src/main/java/com/piuda/callcare/global/config/SecurityConfig.java @@ -44,6 +44,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers(HttpMethod.POST, "/api/conflicts/**").permitAll() .requestMatchers(HttpMethod.POST, "/api/ocr/**").permitAll() .requestMatchers(HttpMethod.POST, "/api/medications/batch").permitAll() + .requestMatchers(HttpMethod.GET, "/api/medications/**").permitAll() .anyRequest().authenticated() ) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); From 7a302461dc964804998a1ebd1ff1040673a90083 Mon Sep 17 00:00:00 2001 From: kangcheolung Date: Wed, 8 Jul 2026 19:17:26 +0900 Subject: [PATCH 2/6] =?UTF-8?q?docs:=20Git=20=EC=9B=8C=ED=81=AC=ED=94=8C?= =?UTF-8?q?=EB=A1=9C=EC=9A=B0=20rules=20=ED=8C=8C=EC=9D=BC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20CLAUDE.md=20=EC=B0=B8=EC=A1=B0=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .claude/rules/git.md 신규 생성 (globs 없이 항상 로드) - 커밋 타이밍, 레이어별 분리 순서, 메시지 작성 규칙 포함 - CLAUDE.md에 git.md 참조 추가 Co-Authored-By: Claude Sonnet 4.6 --- .claude/rules/git.md | 21 +++++++++++++++++++++ CLAUDE.md | 1 + 2 files changed, 22 insertions(+) create mode 100644 .claude/rules/git.md 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/` From 614e2d84f51aad2f1d402a53cd0d5f136f4bc927 Mon Sep 17 00:00:00 2001 From: kangcheolung Date: Wed, 8 Jul 2026 19:39:34 +0900 Subject: [PATCH 3/6] =?UTF-8?q?fix:=20MedicationDetailResponse=20Schema=20?= =?UTF-8?q?=EC=84=A4=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DrugInfo 미연결 시 의약품 정보 필드만 null, drugName은 항상 반환 Co-Authored-By: Claude Sonnet 4.6 --- .../medication/dto/response/MedicationDetailResponse.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index ac13e2d..9caeb76 100644 --- 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 @@ -2,7 +2,7 @@ import io.swagger.v3.oas.annotations.media.Schema; -@Schema(description = "약 상세 정보 응답 — DrugInfo 미연결 시 전 필드 null") +@Schema(description = "약 상세 정보 응답 — drugName은 항상 반환, DrugInfo 미연결 시 의약품 정보 필드 null") public record MedicationDetailResponse( @Schema(description = "약품명") From 2b618ec5c558bca762bb2ce8e2a1f634c2d5fb58 Mon Sep 17 00:00:00 2001 From: kangcheolung Date: Wed, 8 Jul 2026 19:39:40 +0900 Subject: [PATCH 4/6] =?UTF-8?q?feat:=20=EC=95=BD=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=20userId=20=EC=86=8C=EC=9C=A0?= =?UTF-8?q?=EA=B6=8C=20=EA=B2=80=EC=A6=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit userId가 non-null인 경우 medication → senior → user 경로로 소유권 검증 null이면 로컬 테스트용으로 통과 처리 (permitAll 유지 구간) Co-Authored-By: Claude Sonnet 4.6 --- .../medication/service/query/MedicationQueryService.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 index 846a9f7..9ed22d0 100644 --- 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 @@ -18,9 +18,12 @@ public class MedicationQueryService { private final MedicationRepository medicationRepository; private final MedicationConverter medicationConverter; - public MedicationDetailResponse getDetail(Long medicationId) { + 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); } } From be77db2a29463c51c21ee7f289d4001da2107a80 Mon Sep 17 00:00:00 2001 From: kangcheolung Date: Wed, 8 Jul 2026 19:39:45 +0900 Subject: [PATCH 5/6] =?UTF-8?q?feat:=20=EC=95=BD=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=97=94=EB=93=9C=ED=8F=AC=EC=9D=B8?= =?UTF-8?q?=ED=8A=B8=EC=97=90=20@AuthenticationPrincipal=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit userId를 service로 전달하여 소유권 검증 수행 Co-Authored-By: Claude Sonnet 4.6 --- .../domain/medication/controller/MedicationController.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 2d6e928..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 @@ -35,11 +35,12 @@ public ResponseEntity>> registerBatch( return ResponseUtils.created(medicationCommandService.registerBatch(userId, requests)); } - @Operation(summary = "약 상세 정보 조회", description = "약 카드 클릭 시 주의사항/부작용/용법·용량/효능·효과를 반환합니다. DrugInfo 미연결 약은 전 필드 null로 반환됩니다.") + @Operation(summary = "약 상세 정보 조회", description = "약 카드 클릭 시 주의사항/부작용/용법·용량/효능·효과를 반환합니다. DrugInfo 미연결 약은 의약품 정보 필드 null로 반환됩니다.") @GetMapping("/{medicationId}/detail") public ResponseEntity> getDetail( + @AuthenticationPrincipal Long userId, @PathVariable Long medicationId ) { - return ResponseUtils.ok(medicationQueryService.getDetail(medicationId)); + return ResponseUtils.ok(medicationQueryService.getDetail(userId, medicationId)); } } From 0de36dcb274b46cc489cdb071735dfeb25d9db39 Mon Sep 17 00:00:00 2001 From: kangcheolung Date: Wed, 8 Jul 2026 19:39:51 +0900 Subject: [PATCH 6/6] =?UTF-8?q?chore:=20medications=20=EC=97=94=EB=93=9C?= =?UTF-8?q?=ED=8F=AC=EC=9D=B8=ED=8A=B8=20permitAll=EC=97=90=20TODO=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../java/com/piuda/callcare/global/config/SecurityConfig.java | 1 + 1 file changed, 1 insertion(+) 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 2e29278..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,6 +43,7 @@ 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()