merge(#41) 봉사 조회 및 역할 지정 api 추가#44
Conversation
… into feat/(#41)-봉사-조회-및-역할-지정-api-추가
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthrough프로토콜버퍼·gRPC 설정과 Changes
Sequence Diagram(s)sequenceDiagram
actor Client
participant VolunteerWebAdapter as "VolunteerWebAdapter"
participant VolunteerDetailService as "VolunteerDetailService"
participant VolunteerDetailPort as "VolunteerDetailQueryPort"
participant UserQueryPort as "UserQueryPort"
participant AuthGRPC as "Auth Service (gRPC)"
participant Database as "Database"
Client->>VolunteerWebAdapter: GET /volunteers/{volunteerId}
VolunteerWebAdapter->>VolunteerDetailService: execute(volunteerId)
VolunteerDetailService->>VolunteerDetailPort: findDetailByIdOrThrow(volunteerId)
VolunteerDetailPort->>Database: 조회 (volunteer, schedules, participations, activities, userActivities, recurrences)
Database-->>VolunteerDetailPort: VolunteerDetail
VolunteerDetailPort-->>VolunteerDetailService: VolunteerDetail
loop 각 userId에 대해
VolunteerDetailService->>UserQueryPort: getUserName(userId)
UserQueryPort->>AuthGRPC: GetUserName(UserRequest)
AuthGRPC-->>UserQueryPort: UserNameResponse
UserQueryPort-->>VolunteerDetailService: userName or null
end
VolunteerDetailService-->>VolunteerWebAdapter: VolunteerDetailResponse
VolunteerWebAdapter-->>Client: 200 OK
sequenceDiagram
actor Client
participant ActivityWebAdapter as "ActivityWebAdapter"
participant CreateUserActivityService as "CreateUserActivityService"
participant ActivityQueryPort as "ActivityQueryPort"
participant UserActivityCommand as "UserActivityCommandPort"
participant Database as "Database"
Client->>ActivityWebAdapter: POST /activities/user (CreateUserActivityRequest)
ActivityWebAdapter->>CreateUserActivityService: execute(request.toCommand())
loop 각 userActivity 항목
CreateUserActivityService->>ActivityQueryPort: findById(activityId)
ActivityQueryPort->>Database: activity 조회
Database-->>ActivityQueryPort: Activity
ActivityQueryPort-->>CreateUserActivityService: Activity
CreateUserActivityService->>UserActivityCommand: save(UserActivity(userId, activityId))
UserActivityCommand->>Database: insert tbl_user_activity (user_id, activity_id, ...)
Database-->>UserActivityCommand: 저장된 엔티티
UserActivityCommand-->>CreateUserActivityService: UserActivity
end
CreateUserActivityService-->>ActivityWebAdapter: 완료
ActivityWebAdapter-->>Client: 200 OK
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| class ActivityWebAdapter( | ||
| private val createUserActivityUseCase: CreateUserActivityUseCase | ||
| ) { | ||
| @PostMapping("/user") |
There was a problem hiding this comment.
ResponseStatus 추가하시면 좋을 것 같아요
finda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/in/activity/ActivityWebAdapter.kt
Outdated
Show resolved
Hide resolved
...a-volunteer/src/main/kotlin/finda/findavolunteer/adapter/in/volunteer/VolunteerWebAdapter.kt
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
finda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/out/persistence/activity/mapper/UserActivityMapper.kt (1)
25-32:⚠️ Potential issue | 🔴 Criticalmapper에서
activityId존재 검증 필요
CreateUserActivityService는activity!!.id로 activity 존재성을 검증한 후UserActivity를 생성합니다. 그러나toEntity메서드에서는activityRepository.findByIdOrNull(domain.activityId)가null을 반환할 수 있으며, 이 경우activity = null인 엔티티가 저장됩니다. 이후toDomain메서드의requireNotNull(entity.activity?.id)에서IllegalArgumentException이 발생합니다.service 계층의 검증 계약을 준수하기 위해
toEntity에서도 activity 존재성을 명시적으로 검증해야 합니다:수정 제안
override fun toEntity(domain: UserActivity): UserActivityJpaEntity { val activity = requireNotNull(activityRepository.findByIdOrNull(domain.activityId)) { "Activity not found for id: ${domain.activityId}" } return UserActivityJpaEntity( id = domain.id, userId = domain.userId, activity = activity ) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@finda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/out/persistence/activity/mapper/UserActivityMapper.kt` around lines 25 - 32, The toEntity method currently uses activityRepository.findByIdOrNull(domain.activityId) and can return null, allowing a UserActivityJpaEntity with a null activity; update toEntity in UserActivityMapper to explicitly validate presence of the Activity (use requireNotNull or throw a clear exception) when resolving activityRepository.findByIdOrNull(domain.activityId) and only construct UserActivityJpaEntity with a non-null activity, mirroring the service contract enforced before creating UserActivity and preventing downstream requireNotNull failures in toDomain.
🧹 Nitpick comments (7)
finda-volunteer/src/main/resources/db/changelog-volunteer/schema/001-create-volunteer-tables.sql (1)
74-75: 외래 키 제약 조건 이름의 오타도 함께 수정하는 것이 좋습니다.인덱스 이름을
idx_student_participation_volunteer_id로 수정했지만, 74번 라인의 외래 키 제약 조건 이름fk_student_particitation_volunteer에는 여전히 동일한 오타(particitation)가 남아 있습니다. 일관성을 위해 함께 수정을 고려해 주세요.♻️ 외래 키 제약 조건 이름 수정 제안
- CONSTRAINT fk_student_particitation_volunteer + CONSTRAINT fk_student_participation_volunteer FOREIGN KEY (volunteer_id) REFERENCES tbl_volunteer (id) ON DELETE CASCADEAlso applies to: 145-145
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@finda-volunteer/src/main/resources/db/changelog-volunteer/schema/001-create-volunteer-tables.sql` around lines 74 - 75, The foreign key constraint name contains a typo: rename fk_student_particitation_volunteer to fk_student_participation_volunteer to match the corrected index name idx_student_participation_volunteer_id; update all occurrences of fk_student_particitation_volunteer (including the second occurrence noted around the other block) so the CONSTRAINT declaration (FOREIGN KEY (volunteer_id) REFERENCES tbl_volunteer (id) ON DELETE CASCADE) uses the corrected constraint name consistently.finda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/out/persistence/volunteer/repository/VolunteerRepository.kt (1)
24-27: 정렬 방향의 유연성을 고려해 보세요.현재 메서드는
workStartDate DESC로 고정되어 있습니다. PR 이미지에sort_by쿼리 파라미터가 보이는데, 향후 다양한 정렬 옵션(오름차순/내림차순)이 필요하다면 별도의 메서드 추가나 동적 쿼리 구현을 고려해 주세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@finda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/out/persistence/volunteer/repository/VolunteerRepository.kt` around lines 24 - 27, 메서드 findAllByStatusAndYearOrderByWorkStartDateDesc가 workStartDate DESC로 정렬을 고정하고 있어 정렬 방향이 유연하지 않습니다; VolunteerRepository에 정렬 파라미터를 받도록 시그니처를 변경하거나 스프링 데이터의 Sort/Pageable을 사용하여 동적 정렬을 지원하세요 — 예를 들어 메서드 이름에서 OrderBy를 제거하고 findAllByStatusAndYear(status, year, sort: Sort) 또는 findAllByStatusAndYear(status, year, pageable: Pageable) 형태로 바꿔 컨트롤러에서 쿼리 파라미터(sort_by, direction)를 받아 적절한 Sort 객체를 전달하도록 구현하면 됩니다.finda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/out/persistence/volunteer/VolunteerPersistenceAdapter.kt (1)
59-69: FQN 대신 import 사용 권장Line 60에서
VolunteerStatus에 대해 전체 패키지 경로(FQN)를 사용하고 있습니다. 일관성을 위해 상단에 import를 추가하고 클래스명만 사용하는 것이 좋습니다.♻️ 제안된 수정
import 추가:
import finda.findavolunteer.domain.volunteer.enum.VolunteerStatusoverride fun findAll( - status: finda.findavolunteer.domain.volunteer.enum.VolunteerStatus?, + status: VolunteerStatus?, year: Int?, sortBy: VolunteerSortBy ): List<Volunteer> {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@finda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/out/persistence/volunteer/VolunteerPersistenceAdapter.kt` around lines 59 - 69, The parameter type uses the fully-qualified name for VolunteerStatus in VolunteerPersistenceAdapter.findAll; replace the FQN with an import by adding "import finda.findavolunteer.domain.volunteer.enum.VolunteerStatus" at the top and change the method signature to use VolunteerStatus directly (affecting the signature of findAll and any references like volunteerRepository.findAllByStatusAndYearOrderByWorkStartDateDesc and volunteerMapper::toDomain).finda-volunteer/src/main/kotlin/finda/findavolunteer/application/service/volunteer/VolunteerDetailService.kt (2)
48-68: 사용자 이름 조회 시 N+1 문제 발생 가능각
studentParticipation과userActivity에 대해 개별적으로userQueryPort.getUserName(userId)을 호출하고 있습니다. 참여자가 많을 경우 다수의 gRPC 호출이 발생하여 응답 시간이 증가할 수 있습니다.배치 조회 메서드(예:
getUserNames(userIds: List<UUID>))를 도입하여 한 번의 호출로 모든 사용자 이름을 가져오는 것을 고려해보세요.♻️ 개선 방향 예시
// UserQueryPort에 배치 메서드 추가 fun getUserNames(userIds: List<UUID>): Map<UUID, String> // VolunteerDetailService에서 사용 val allUserIds = (volunteerDetail.studentParticipations.map { it.userId } + volunteerDetail.userActivities.map { it.userId }).distinct() val userNameMap = userQueryPort.getUserNames(allUserIds) // 매핑 시 Map에서 조회 studentParticipations = volunteerDetail.studentParticipations.map { StudentParticipationResponse( userId = it.userId, name = userNameMap[it.userId] ?: throw UserNotFoundException, // ... ) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@finda-volunteer/src/main/kotlin/finda/findavolunteer/application/service/volunteer/VolunteerDetailService.kt` around lines 48 - 68, The code in VolunteerDetailService calls userQueryPort.getUserName for each volunteerDetail.studentParticipations and volunteerDetail.userActivities causing potential N+1 gRPC calls; add a batch lookup on UserQueryPort (e.g., getUserNames(userIds: List<UUID>): Map<UUID, String>), collect distinct userIds from volunteerDetail.studentParticipations and volunteerDetail.userActivities, call the new batch method once to get a userNameMap, and replace per-item getUserName calls with lookups like userNameMap[it.userId] ?: throw UserNotFoundException to populate StudentParticipationResponse and UserActivityResponse.
20-70: 읽기 전용 트랜잭션 누락 확인서비스 메서드에
@Transactional(readOnly = true)어노테이션이 없습니다. 읽기 전용 트랜잭션을 명시하면 JPA 더티 체킹을 비활성화하여 성능이 향상될 수 있습니다.♻️ 제안된 수정
+import org.springframework.transaction.annotation.Transactional + `@Service` class VolunteerDetailService( private val volunteerDetailQueryPort: VolunteerDetailQueryPort, private val userQueryPort: UserQueryPort ) : VolunteerDetailUseCase { + `@Transactional`(readOnly = true) override fun execute(volunteerId: UUID): VolunteerDetailResponse {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@finda-volunteer/src/main/kotlin/finda/findavolunteer/application/service/volunteer/VolunteerDetailService.kt` around lines 20 - 70, Add a read-only transaction to the volunteer detail retrieval: annotate the VolunteerDetailService.execute method (or the VolunteerDetailService class) with `@Transactional`(readOnly = true) and import org.springframework.transaction.annotation.Transactional so JPA dirty checking is disabled for this read operation; ensure the annotation is placed on the overridden execute(volunteerId: UUID) function (or class) that calls volunteerDetailQueryPort.findDetailByIdOrThrow and userQueryPort.getUserName.finda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/out/persistence/volunteer/VolunteerDetailPersistenceAdapter.kt (1)
32-105: 여러 개의 별도 Repository 호출로 인한 성능 고려하나의 봉사 상세 조회에 7개의 Repository 호출이 발생합니다. 현재 구현은 기능적으로 정확하지만, 트래픽이 증가하면 단일 쿼리 또는 배치 조회로 최적화를 고려해볼 수 있습니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@finda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/out/persistence/volunteer/VolunteerDetailPersistenceAdapter.kt` around lines 32 - 105, findDetailByIdOrThrow currently issues seven separate repository calls (volunteerRepository, volunteerScheduleRepository, studentParticipationRepository, activityRepository, userActivityRepository, activityRecurrenceWeekRepository, activityRecurrenceMonthRepository), which will hurt performance under load; refactor by consolidating these into fewer queries — e.g., add a custom query or repository method (on VolunteerRepository or a new VolunteerDetailQueryRepository) that fetches the Volunteer and its related collections via JOIN FETCH/entity-graph or returns a projection/DTO mapping to VolunteerDetail, or use batched IN queries and a mapper to assemble VolunteerDetail in one pass; keep the public API (findDetailByIdOrThrow) but replace the multiple repository calls with the new consolidated query method and adapt mapping of Volunteer, schedules, activities, userActivities, weekdays, monthDate and studentParticipations accordingly.finda-volunteer/src/main/kotlin/finda/findavolunteer/application/service/activity/CreateUserActivityService.kt (1)
21-25:it.activityId를 직접 사용 가능
activity.id는 이미it.activityId와 동일한 값입니다. Activity 엔티티 조회는 존재 검증 목적으로만 사용되고 있으므로, 조회 후activity.id대신it.activityId를 직접 사용할 수 있습니다.♻️ 제안된 수정
request.userActivityList.forEach { - val activity = activityQueryPort.findById(it.activityId) + activityQueryPort.findById(it.activityId) + ?: throw ActivityNotFoundException userActivityCommandPort.save( UserActivity( - activityId = activity!!.id, + activityId = it.activityId, userId = it.userId ) ) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@finda-volunteer/src/main/kotlin/finda/findavolunteer/application/service/activity/CreateUserActivityService.kt` around lines 21 - 25, UserActivity 생성 시 activity!!.id 대신 it.activityId를 직접 사용하도록 변경하세요: UserActivity(activityId = it.activityId, userId = it.userId). Activity 엔티티 조회는 여전히 존재 검증용으로 유지하되, 생성자에는 조회된 activity의 id를 사용하지 않아 널 강제(!!)를 제거하고 it.activityId로 대체해 안전하게 처리하세요 (참조: CreateUserActivityService, UserActivity, activity, it.activityId).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@finda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/in/volunteer/VolunteerWebAdapter.kt`:
- Around line 50-51: 삭제 엔드포인트인 VolunteerWebAdapter의 deleteVolunteer 메서드는
`@ResponseStatus`(HttpStatus.NO_CONTENT) 어노테이션이 빠져 기본 응답코드가 200으로 바뀌었습니다;
deleteVolunteer(`@RequestParam` volunteerId: UUID) 메서드에
`@ResponseStatus`(HttpStatus.NO_CONTENT) 어노테이션을 복원하여 기존 API 계약(204 NO_CONTENT)을
유지하고, 기존 호출 흐름인 deleteVolunteerUseCase.execute(volunteerId)를 변경하지 마세요.
In
`@finda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/out/grpc/UserGrpcAdapter.kt`:
- Around line 35-36: In UserGrpcAdapter (around the log.error("gRPC getUserName
failed. userId=$userId", e) and subsequent throw e), avoid logging the raw
userId; instead compute a non-reversible or masked representation (e.g., SHA-256
hash or mask middle characters) and log that token (e.g., hashedUserId or
maskedUserId) in the error message, keeping the same context and rethrowing the
exception unchanged; update the log call to use the sanitized identifier
variable rather than userId.
In
`@finda-volunteer/src/main/kotlin/finda/findavolunteer/application/port/in/volunteer/VolunteerDetailUseCase.kt`:
- Around line 3-7: The VolunteerDetailUseCase interface currently depends on an
adapter-layer DTO (VolunteerDetailResponse) which inverts layer dependencies;
move the DTO from adapter.`in`.volunteer.dto.response to the application layer
(e.g., application.port.in.volunteer.dto) and update the import in
VolunteerDetailUseCase to reference the new package, then update the adapter
implementation(s) and any mappers to construct/convert to the new
application-layer VolunteerDetailResponse so only the adapter depends on the
application contract (keep the interface signature fun execute(volunteerId:
UUID): VolunteerDetailResponse unchanged).
In
`@finda-volunteer/src/main/kotlin/finda/findavolunteer/application/port/in/volunteer/VolunteerListUseCase.kt`:
- Around line 3-11: The VolunteerListUseCase interface currently returns the
web-layer DTO VolunteerListResponse (remove the import and dependency) — change
the execute(...) signature to return an application/domain model (e.g.,
VolunteerSummary or VolunteerDto defined in the application/domain package)
instead of VolunteerListResponse, and keep parameters (status, year, sortBy)
unchanged; then implement mapping from that domain/application model to
adapter.in.volunteer.dto.response.VolunteerListResponse inside the web adapter
(controller/mapper) so the port no longer depends on adapter types and
VolunteerListUseCase remains layer-pure.
In
`@finda-volunteer/src/main/kotlin/finda/findavolunteer/application/service/activity/CreateUserActivityService.kt`:
- Around line 17-27: The loop in CreateUserActivityService.execute uses
activityQueryPort.findById and then activity!!.id which causes a
KotlinNullPointerException when not found; replace this with a null-safe lookup
that throws a clear "not found" error per convention: call
activityQueryPort.findByIdOrThrow(it.activityId) if that method exists, or
perform an explicit null check on the result of
activityQueryPort.findById(it.activityId) and throw a domain NotFoundException
(or similar) with a descriptive message before constructing UserActivity and
calling userActivityCommandPort.save(UserActivity(...)).
In `@finda-volunteer/src/main/proto/auth.proto`:
- Line 3: The package declaration "package auth;" in auth.proto doesn't match
the file's directory layout and triggers Buf's PACKAGE_DIRECTORY_MATCH error;
fix it by either moving auth.proto into a directory named auth so the path
matches the package, or by updating the package declaration to the correct
package name that reflects the current directory structure (replace "package
auth;" accordingly) and update any import/option references that depend on that
package.
In `@finda-volunteer/src/main/resources/application.yml`:
- Around line 7-11: The default gRPC client negotiation-type is set to plaintext
for auth-service in application.yml, which enables non-TLS traffic in
production; update the configuration so production enforces TLS by either
changing the default negotiation-type under grpc.client.auth-service to TLS or
make negotiation-type configurable via an environment variable (e.g.
AUTH_GRPC_NEGOTIATION_TYPE) and ensure application-prod.yml explicitly sets
grpc.client.auth-service.negotiation-type: TLS to force TLS/mTLS in production;
locate the negotiation-type setting in application.yml and add/adjust the
corresponding override in application-prod.yml or read from the env var.
In
`@finda-volunteer/src/main/resources/db/changelog-volunteer/schema/002-add-user-id-to-user-activity.sql`:
- Around line 4-5: The migration should not add user_id as NOT NULL in one step;
instead modify the changelog to add the column as nullable first on
tbl_user_activity (ADD COLUMN user_id BINARY(16) NULL), perform a separate
backfill step that updates tbl_user_activity.user_id with correct values (or a
default/mapped value) in a follow-up changeSet, and finally add a third
changeSet that alters the column to NOT NULL (ALTER TABLE tbl_user_activity
MODIFY COLUMN user_id BINARY(16) NOT NULL) after verifying no NULLs remain;
reference the table name tbl_user_activity and column user_id when implementing
these staged changeSets.
In
`@finda-volunteer/src/main/resources/db/changelog-volunteer/schema/003-add-unique-teacher-participation-user-id.sql`:
- Around line 4-5: The migration adds a UNIQUE(user_id) constraint
(uk_teacher_participation_user_id) on tbl_teacher_participation which conflicts
with the repository design (TeacherParticipationRepository.findAllByUserId
returning List<TeacherParticipationJpaEntity>) and prevents a teacher from
having multiple participation records; update the migration to either drop this
UNIQUE constraint or replace it with a composite unique on (user_id,
volunteer_id) to match tbl_student_participation semantics and allow multiple
participations per user, ensuring the constraint name
(uk_teacher_participation_user_id) and table (tbl_teacher_participation) are
adjusted accordingly.
---
Outside diff comments:
In
`@finda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/out/persistence/activity/mapper/UserActivityMapper.kt`:
- Around line 25-32: The toEntity method currently uses
activityRepository.findByIdOrNull(domain.activityId) and can return null,
allowing a UserActivityJpaEntity with a null activity; update toEntity in
UserActivityMapper to explicitly validate presence of the Activity (use
requireNotNull or throw a clear exception) when resolving
activityRepository.findByIdOrNull(domain.activityId) and only construct
UserActivityJpaEntity with a non-null activity, mirroring the service contract
enforced before creating UserActivity and preventing downstream requireNotNull
failures in toDomain.
---
Nitpick comments:
In
`@finda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/out/persistence/volunteer/repository/VolunteerRepository.kt`:
- Around line 24-27: 메서드 findAllByStatusAndYearOrderByWorkStartDateDesc가
workStartDate DESC로 정렬을 고정하고 있어 정렬 방향이 유연하지 않습니다; VolunteerRepository에 정렬 파라미터를
받도록 시그니처를 변경하거나 스프링 데이터의 Sort/Pageable을 사용하여 동적 정렬을 지원하세요 — 예를 들어 메서드 이름에서
OrderBy를 제거하고 findAllByStatusAndYear(status, year, sort: Sort) 또는
findAllByStatusAndYear(status, year, pageable: Pageable) 형태로 바꿔 컨트롤러에서 쿼리
파라미터(sort_by, direction)를 받아 적절한 Sort 객체를 전달하도록 구현하면 됩니다.
In
`@finda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/out/persistence/volunteer/VolunteerDetailPersistenceAdapter.kt`:
- Around line 32-105: findDetailByIdOrThrow currently issues seven separate
repository calls (volunteerRepository, volunteerScheduleRepository,
studentParticipationRepository, activityRepository, userActivityRepository,
activityRecurrenceWeekRepository, activityRecurrenceMonthRepository), which will
hurt performance under load; refactor by consolidating these into fewer queries
— e.g., add a custom query or repository method (on VolunteerRepository or a new
VolunteerDetailQueryRepository) that fetches the Volunteer and its related
collections via JOIN FETCH/entity-graph or returns a projection/DTO mapping to
VolunteerDetail, or use batched IN queries and a mapper to assemble
VolunteerDetail in one pass; keep the public API (findDetailByIdOrThrow) but
replace the multiple repository calls with the new consolidated query method and
adapt mapping of Volunteer, schedules, activities, userActivities, weekdays,
monthDate and studentParticipations accordingly.
In
`@finda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/out/persistence/volunteer/VolunteerPersistenceAdapter.kt`:
- Around line 59-69: The parameter type uses the fully-qualified name for
VolunteerStatus in VolunteerPersistenceAdapter.findAll; replace the FQN with an
import by adding "import
finda.findavolunteer.domain.volunteer.enum.VolunteerStatus" at the top and
change the method signature to use VolunteerStatus directly (affecting the
signature of findAll and any references like
volunteerRepository.findAllByStatusAndYearOrderByWorkStartDateDesc and
volunteerMapper::toDomain).
In
`@finda-volunteer/src/main/kotlin/finda/findavolunteer/application/service/activity/CreateUserActivityService.kt`:
- Around line 21-25: UserActivity 생성 시 activity!!.id 대신 it.activityId를 직접 사용하도록
변경하세요: UserActivity(activityId = it.activityId, userId = it.userId). Activity
엔티티 조회는 여전히 존재 검증용으로 유지하되, 생성자에는 조회된 activity의 id를 사용하지 않아 널 강제(!!)를 제거하고
it.activityId로 대체해 안전하게 처리하세요 (참조: CreateUserActivityService, UserActivity,
activity, it.activityId).
In
`@finda-volunteer/src/main/kotlin/finda/findavolunteer/application/service/volunteer/VolunteerDetailService.kt`:
- Around line 48-68: The code in VolunteerDetailService calls
userQueryPort.getUserName for each volunteerDetail.studentParticipations and
volunteerDetail.userActivities causing potential N+1 gRPC calls; add a batch
lookup on UserQueryPort (e.g., getUserNames(userIds: List<UUID>): Map<UUID,
String>), collect distinct userIds from volunteerDetail.studentParticipations
and volunteerDetail.userActivities, call the new batch method once to get a
userNameMap, and replace per-item getUserName calls with lookups like
userNameMap[it.userId] ?: throw UserNotFoundException to populate
StudentParticipationResponse and UserActivityResponse.
- Around line 20-70: Add a read-only transaction to the volunteer detail
retrieval: annotate the VolunteerDetailService.execute method (or the
VolunteerDetailService class) with `@Transactional`(readOnly = true) and import
org.springframework.transaction.annotation.Transactional so JPA dirty checking
is disabled for this read operation; ensure the annotation is placed on the
overridden execute(volunteerId: UUID) function (or class) that calls
volunteerDetailQueryPort.findDetailByIdOrThrow and userQueryPort.getUserName.
In
`@finda-volunteer/src/main/resources/db/changelog-volunteer/schema/001-create-volunteer-tables.sql`:
- Around line 74-75: The foreign key constraint name contains a typo: rename
fk_student_particitation_volunteer to fk_student_participation_volunteer to
match the corrected index name idx_student_participation_volunteer_id; update
all occurrences of fk_student_particitation_volunteer (including the second
occurrence noted around the other block) so the CONSTRAINT declaration (FOREIGN
KEY (volunteer_id) REFERENCES tbl_volunteer (id) ON DELETE CASCADE) uses the
corrected constraint name consistently.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: a7053def-c62c-4e46-aece-f30d62ac0775
📒 Files selected for processing (48)
finda-volunteer/build.gradle.ktsfinda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/in/.gitkeepfinda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/in/activity/ActivityWebAdapter.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/in/activity/dto/request/CreateUserActivityRequest.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/in/volunteer/VolunteerWebAdapter.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/in/volunteer/dto/response/VolunteerDetailResponse.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/in/volunteer/dto/response/VolunteerListResponse.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/out/grpc/UserGrpcAdapter.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/out/persistence/activity/ActivityPersistenceAdapter.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/out/persistence/activity/UserActivityAdapter.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/out/persistence/activity/entity/UserActivityJpaEntity.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/out/persistence/activity/mapper/UserActivityMapper.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/out/persistence/activity/repository/ActivityRepository.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/out/persistence/activity/repository/UserActivityRepository.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/out/persistence/participation/repository/StudentParticipationRepository.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/out/persistence/volunteer/VolunteerDetailPersistenceAdapter.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/out/persistence/volunteer/VolunteerPersistenceAdapter.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/out/persistence/volunteer/repository/ActivityRecurrenceMonthRepository.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/out/persistence/volunteer/repository/ActivityRecurrenceWeekRepository.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/out/persistence/volunteer/repository/VolunteerRepository.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/out/persistence/volunteer/repository/VolunteerScheduleRepository.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/application/exception/InvalidVolunteerSortByException.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/application/exception/UserNotFoundException.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/application/port/in/.gitkeepfinda-volunteer/src/main/kotlin/finda/findavolunteer/application/port/in/activity/CreateUserActivityUseCase.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/application/port/in/volunteer/VolunteerDetailUseCase.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/application/port/in/volunteer/VolunteerListUseCase.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/application/port/out/.gitkeepfinda-volunteer/src/main/kotlin/finda/findavolunteer/application/port/out/activity/UserActivityCommandPort.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/application/port/out/activity/UserActivityQueryPort.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/application/port/out/user/UserQueryPort.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/application/port/out/volunteer/VolunteerDetailQueryPort.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/application/port/out/volunteer/VolunteerListQueryPort.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/application/service/.gitkeepfinda-volunteer/src/main/kotlin/finda/findavolunteer/application/service/activity/CreateUserActivityService.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/application/service/volunteer/VolunteerDetailService.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/application/service/volunteer/VolunteerListService.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/domain/.gitkeepfinda-volunteer/src/main/kotlin/finda/findavolunteer/domain/activity/model/UserActivity.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/domain/volunteer/enum/VolunteerSortBy.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/domain/volunteer/model/VolunteerDetail.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/global/error/exception/ErrorCode.ktfinda-volunteer/src/main/proto/auth.protofinda-volunteer/src/main/resources/application.ymlfinda-volunteer/src/main/resources/db/changelog-volunteer/master.xmlfinda-volunteer/src/main/resources/db/changelog-volunteer/schema/001-create-volunteer-tables.sqlfinda-volunteer/src/main/resources/db/changelog-volunteer/schema/002-add-user-id-to-user-activity.sqlfinda-volunteer/src/main/resources/db/changelog-volunteer/schema/003-add-unique-teacher-participation-user-id.sql
...a-volunteer/src/main/kotlin/finda/findavolunteer/adapter/in/volunteer/VolunteerWebAdapter.kt
Show resolved
Hide resolved
finda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/out/grpc/UserGrpcAdapter.kt
Outdated
Show resolved
Hide resolved
| import finda.findavolunteer.adapter.`in`.volunteer.dto.response.VolunteerDetailResponse | ||
| import java.util.UUID | ||
|
|
||
| interface VolunteerDetailUseCase { | ||
| fun execute(volunteerId: UUID): VolunteerDetailResponse |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
애플리케이션 포트가 adapter.in DTO에 의존하고 있습니다.
계층 의존 방향이 뒤집혀 어댑터 변경이 유스케이스 계약에 전파될 수 있습니다. 응답 DTO를 application 계층(예: application.port.in.volunteer.dto)으로 이동해 경계를 정리하는 것을 권장합니다.
♻️ 의존 방향 정리 예시
-import finda.findavolunteer.adapter.`in`.volunteer.dto.response.VolunteerDetailResponse
+import finda.findavolunteer.application.port.`in`.volunteer.dto.response.VolunteerDetailResponse🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@finda-volunteer/src/main/kotlin/finda/findavolunteer/application/port/in/volunteer/VolunteerDetailUseCase.kt`
around lines 3 - 7, The VolunteerDetailUseCase interface currently depends on an
adapter-layer DTO (VolunteerDetailResponse) which inverts layer dependencies;
move the DTO from adapter.`in`.volunteer.dto.response to the application layer
(e.g., application.port.in.volunteer.dto) and update the import in
VolunteerDetailUseCase to reference the new package, then update the adapter
implementation(s) and any mappers to construct/convert to the new
application-layer VolunteerDetailResponse so only the adapter depends on the
application contract (keep the interface signature fun execute(volunteerId:
UUID): VolunteerDetailResponse unchanged).
...r/src/main/kotlin/finda/findavolunteer/application/port/in/volunteer/VolunteerListUseCase.kt
Outdated
Show resolved
Hide resolved
| override fun execute(request: CreateUserActivityRequest) { | ||
| request.userActivityList.forEach { | ||
| val activity = activityQueryPort.findById(it.activityId) | ||
| userActivityCommandPort.save( | ||
| UserActivity( | ||
| activityId = activity!!.id, | ||
| userId = it.userId | ||
| ) | ||
| ) | ||
| } | ||
| } |
There was a problem hiding this comment.
Activity 조회 실패 시 NPE 대신 명확한 예외 처리 필요
activity!!.id에서 activityQueryPort.findById가 null을 반환하면 KotlinNullPointerException이 발생합니다. 이는 클라이언트에게 명확한 오류 메시지를 제공하지 못합니다.
프로젝트 컨벤션에 따라 findByIdOrThrow 메서드를 사용하거나, 명시적인 null 체크와 함께 커스텀 예외를 던지는 것이 좋습니다.
🐛 제안된 수정
`@Transactional`
override fun execute(request: CreateUserActivityRequest) {
request.userActivityList.forEach {
- val activity = activityQueryPort.findById(it.activityId)
+ val activity = activityQueryPort.findById(it.activityId)
+ ?: throw ActivityNotFoundException
userActivityCommandPort.save(
UserActivity(
- activityId = activity!!.id,
+ activityId = activity.id,
userId = it.userId
)
)
}
}Based on learnings, exception-throwing "not found" behavior must be implemented in separate methods suffixed with OrThrow, or handled explicitly with a meaningful exception.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@finda-volunteer/src/main/kotlin/finda/findavolunteer/application/service/activity/CreateUserActivityService.kt`
around lines 17 - 27, The loop in CreateUserActivityService.execute uses
activityQueryPort.findById and then activity!!.id which causes a
KotlinNullPointerException when not found; replace this with a null-safe lookup
that throws a clear "not found" error per convention: call
activityQueryPort.findByIdOrThrow(it.activityId) if that method exists, or
perform an explicit null check on the result of
activityQueryPort.findById(it.activityId) and throw a domain NotFoundException
(or similar) with a descriptive message before constructing UserActivity and
calling userActivityCommandPort.save(UserActivity(...)).
| @@ -0,0 +1,42 @@ | |||
| syntax = "proto3"; | |||
|
|
|||
| package auth; | |||
There was a problem hiding this comment.
패키지-디렉터리 불일치로 Buf lint가 실패합니다.
Line 3의 package auth; 선언이 현재 파일 위치(finda-volunteer/src/main/proto/auth.proto)와 맞지 않아 PACKAGE_DIRECTORY_MATCH 오류가 발생합니다. 파일을 finda-volunteer/src/main/proto/auth/auth.proto로 이동하거나, 패키지명을 실제 디렉터리 구조와 일치시키는 수정이 필요합니다.
🧰 Tools
🪛 Buf (1.66.1)
[error] 3-3: Files with package "auth" must be within a directory "auth" relative to root but were in directory "finda-volunteer/src/main/proto".
(PACKAGE_DIRECTORY_MATCH)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@finda-volunteer/src/main/proto/auth.proto` at line 3, The package declaration
"package auth;" in auth.proto doesn't match the file's directory layout and
triggers Buf's PACKAGE_DIRECTORY_MATCH error; fix it by either moving auth.proto
into a directory named auth so the path matches the package, or by updating the
package declaration to the correct package name that reflects the current
directory structure (replace "package auth;" accordingly) and update any
import/option references that depend on that package.
| grpc: | ||
| client: | ||
| auth-service: | ||
| address: ${AUTH_GRPC_ADDRESS} | ||
| negotiation-type: plaintext |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "application*.yml" -o -name "application*.yaml" | head -20Repository: team-FINDA/Finda-Backend
Length of output: 883
🏁 Script executed:
cat -n finda-volunteer/src/main/resources/application.ymlRepository: team-FINDA/Finda-Backend
Length of output: 1231
🏁 Script executed:
ls -la finda-volunteer/src/main/resources/ | grep applicationRepository: team-FINDA/Finda-Backend
Length of output: 268
🏁 Script executed:
cat -n finda-volunteer/src/main/resources/application-dev.ymlRepository: team-FINDA/Finda-Backend
Length of output: 541
🏁 Script executed:
cat -n finda-volunteer/src/main/resources/application-prod.ymlRepository: team-FINDA/Finda-Backend
Length of output: 543
🏁 Script executed:
rg "AUTH_GRPC_NEGOTIATION_TYPE" --type yaml --type ymlRepository: team-FINDA/Finda-Backend
Length of output: 95
🏁 Script executed:
rg "negotiation-type" finda-volunteer/src/main/resources/Repository: team-FINDA/Finda-Backend
Length of output: 152
plaintext gRPC 설정이 운영 환경에도 적용됩니다.
기본 application.yml의 negotiation-type: plaintext는 application-prod.yml에서 오버라이드되지 않으므로, 운영 환경에서도 TLS 암호화 없이 gRPC 통신이 진행됩니다. 마이크로서비스 간 민감한 인증 통신이므로 운영 환경에서는 TLS/mTLS를 강제해야 합니다.
기본값을 TLS로 변경하거나, 운영 프로필에서 명시적으로 TLS를 설정하세요. 또는 환경 변수로 제어 가능하도록 수정하세요.
💡 권장 수정 방법
grpc:
client:
auth-service:
address: ${AUTH_GRPC_ADDRESS}
- negotiation-type: plaintext
+ negotiation-type: ${AUTH_GRPC_NEGOTIATION_TYPE:TLS}# application-prod.yml
grpc:
client:
auth-service:
negotiation-type: TLS📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| grpc: | |
| client: | |
| auth-service: | |
| address: ${AUTH_GRPC_ADDRESS} | |
| negotiation-type: plaintext | |
| grpc: | |
| client: | |
| auth-service: | |
| address: ${AUTH_GRPC_ADDRESS} | |
| negotiation-type: ${AUTH_GRPC_NEGOTIATION_TYPE:TLS} |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@finda-volunteer/src/main/resources/application.yml` around lines 7 - 11, The
default gRPC client negotiation-type is set to plaintext for auth-service in
application.yml, which enables non-TLS traffic in production; update the
configuration so production enforces TLS by either changing the default
negotiation-type under grpc.client.auth-service to TLS or make negotiation-type
configurable via an environment variable (e.g. AUTH_GRPC_NEGOTIATION_TYPE) and
ensure application-prod.yml explicitly sets
grpc.client.auth-service.negotiation-type: TLS to force TLS/mTLS in production;
locate the negotiation-type setting in application.yml and add/adjust the
corresponding override in application-prod.yml or read from the env var.
| ALTER TABLE tbl_user_activity | ||
| ADD COLUMN user_id BINARY(16) NOT NULL; |
There was a problem hiding this comment.
기존 데이터가 있으면 마이그레이션이 실패할 수 있습니다.
NOT NULL 컬럼을 바로 추가하면 기존 tbl_user_activity 데이터 때문에 DDL이 실패할 수 있습니다. NULL 허용으로 추가 → 백필 → NOT NULL 전환의 2단계(또는 3단계)로 분리하는 것이 안전합니다.
💡 안전한 마이그레이션 예시
ALTER TABLE tbl_user_activity
- ADD COLUMN user_id BINARY(16) NOT NULL;
+ ADD COLUMN user_id BINARY(16) NULL;
+
+-- backfill 수행 (서비스 정책에 맞는 user_id 채움)
+-- UPDATE tbl_user_activity SET user_id = ... WHERE user_id IS NULL;
+
+ALTER TABLE tbl_user_activity
+ MODIFY COLUMN user_id BINARY(16) NOT NULL;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@finda-volunteer/src/main/resources/db/changelog-volunteer/schema/002-add-user-id-to-user-activity.sql`
around lines 4 - 5, The migration should not add user_id as NOT NULL in one
step; instead modify the changelog to add the column as nullable first on
tbl_user_activity (ADD COLUMN user_id BINARY(16) NULL), perform a separate
backfill step that updates tbl_user_activity.user_id with correct values (or a
default/mapped value) in a follow-up changeSet, and finally add a third
changeSet that alters the column to NOT NULL (ALTER TABLE tbl_user_activity
MODIFY COLUMN user_id BINARY(16) NOT NULL) after verifying no NULLs remain;
reference the table name tbl_user_activity and column user_id when implementing
these staged changeSets.
| ALTER TABLE tbl_teacher_participation | ||
| ADD CONSTRAINT uk_teacher_participation_user_id UNIQUE (user_id); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# First, let's explore the changelog directory structure
find . -type f -name "*.sql" | head -20Repository: team-FINDA/Finda-Backend
Length of output: 904
🏁 Script executed:
#!/bin/bash
# Get the full path to the schema directory mentioned in the file
git ls-files | grep -i "changelog-volunteer" | head -20Repository: team-FINDA/Finda-Backend
Length of output: 595
🏁 Script executed:
#!/bin/bash
# Look at the specific migration file and surrounding context
git ls-files | grep -i "003-add-unique"Repository: team-FINDA/Finda-Backend
Length of output: 181
🏁 Script executed:
#!/bin/bash
# Search for other migration files related to teacher_participation
find . -type f -name "*.sql" -exec grep -l "teacher_participation" {} \;Repository: team-FINDA/Finda-Backend
Length of output: 282
🏁 Script executed:
#!/bin/bash
# Read the table creation migration file to understand the schema
cat ./finda-volunteer/src/main/resources/db/changelog-volunteer/schema/001-create-volunteer-tables.sqlRepository: team-FINDA/Finda-Backend
Length of output: 6027
🏁 Script executed:
#!/bin/bash
# Read the constraint addition migration file
cat ./finda-volunteer/src/main/resources/db/changelog-volunteer/schema/003-add-unique-teacher-participation-user-id.sqlRepository: team-FINDA/Finda-Backend
Length of output: 270
🏁 Script executed:
#!/bin/bash
# Search for Java code that references teacher_participation
find . -type f \( -name "*.java" -o -name "*.kt" \) -exec grep -l "teacher_participation\|TeacherParticipation" {} \;Repository: team-FINDA/Finda-Backend
Length of output: 1293
🏁 Script executed:
#!/bin/bash
# Check the TeacherParticipationJpaEntity to understand the business logic
cat ./finda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/out/persistence/participation/entity/TeacherParticipationJpaEntity.ktRepository: team-FINDA/Finda-Backend
Length of output: 839
🏁 Script executed:
#!/antml:bash
# Check the repository to see query patterns
cat ./finda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/out/persistence/participation/repository/TeacherParticipationRepository.ktRepository: team-FINDA/Finda-Backend
Length of output: 565
🏁 Script executed:
#!/bin/bash
# Check the domain model
cat ./finda-volunteer/src/main/kotlin/finda/findavolunteer/domain/participation/model/TeacherParticipation.ktRepository: team-FINDA/Finda-Backend
Length of output: 261
🏁 Script executed:
#!/bin/bash
# Check how teacher participation is used in service
cat ./finda-volunteer/src/main/kotlin/finda/findavolunteer/application/service/volunteer/CreateVolunteerService.ktRepository: team-FINDA/Finda-Backend
Length of output: 5009
🏁 Script executed:
#!/bin/bash
# Check if there are any other migrations related to teacher_participation
find . -type f -name "*.sql" -o -name "*.xml" | xargs grep -l "teacher_participation" 2>/dev/null | head -20Repository: team-FINDA/Finda-Backend
Length of output: 282
🏁 Script executed:
#!/bin/bash
# Check if there are other uses of TeacherParticipationQueryPort
find . -type f -name "*.kt" -exec grep -l "TeacherParticipationQueryPort\|TeacherParticipationCommandPort" {} \;Repository: team-FINDA/Finda-Backend
Length of output: 550
🏁 Script executed:
#!/bin/bash
# Look at the query port to see what operations are expected
cat ./finda-volunteer/src/main/kotlin/finda/findavolunteer/application/port/out/participation/TeacherParticipationQueryPort.ktRepository: team-FINDA/Finda-Backend
Length of output: 394
🏁 Script executed:
#!/bin/bash
# Check the adapter implementation to see how it uses the query port
cat ./finda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/out/persistence/participation/TeacherParticipationAdapter.ktRepository: team-FINDA/Finda-Backend
Length of output: 1698
🏁 Script executed:
#!/bin/bash
# Check command port to understand save behavior
cat ./finda-volunteer/src/main/kotlin/finda/findavolunteer/application/port/out/participation/TeacherParticipationCommandPort.ktRepository: team-FINDA/Finda-Backend
Length of output: 334
UNIQUE(user_id) 제약이 레파지토리 설계와 충돌합니다.
TeacherParticipationRepository.findAllByUserId(userId)가 List<TeacherParticipationJpaEntity>를 반환하는 것은 사용자당 여러 레코드를 처리할 수 있도록 설계되었음을 의미합니다. 그러나 UNIQUE(user_id) 제약은 사용자당 최대 1개 레코드만 허용하므로, 같은 교사가 여러 봉사활동에 참여할 수 없습니다. tbl_student_participation과 달리 이 제약이 있는 이유가 명확하지 않으므로, (user_id, volunteer_id) 복합 유니크 키로 변경하거나 제약을 제거해야 합니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@finda-volunteer/src/main/resources/db/changelog-volunteer/schema/003-add-unique-teacher-participation-user-id.sql`
around lines 4 - 5, The migration adds a UNIQUE(user_id) constraint
(uk_teacher_participation_user_id) on tbl_teacher_participation which conflicts
with the repository design (TeacherParticipationRepository.findAllByUserId
returning List<TeacherParticipationJpaEntity>) and prevents a teacher from
having multiple participation records; update the migration to either drop this
UNIQUE constraint or replace it with a composite unique on (user_id,
volunteer_id) to match tbl_student_participation semantics and allow multiple
participations per user, ensuring the constraint name
(uk_teacher_participation_user_id) and table (tbl_teacher_participation) are
adjusted accordingly.
… into feat/(#41)-봉사-조회-및-역할-지정-api-추가
There was a problem hiding this comment.
♻️ Duplicate comments (1)
finda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/in/activity/ActivityWebAdapter.kt (1)
16-21:⚠️ Potential issue | 🟡 Minor생성 API는 201 상태 코드를 명시해 주세요.
Line [16]~Line [21]은 리소스 생성 동작인데 현재 기본 200 응답이 내려갈 수 있습니다.
@ResponseStatus(HttpStatus.CREATED)또는ResponseEntity.created(...)로 계약을 명확히 하는 게 좋습니다.제안 diff
import jakarta.validation.Valid +import org.springframework.http.HttpStatus import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController @@ - `@PostMapping`("/user") + `@PostMapping`("/user") + `@ResponseStatus`(HttpStatus.CREATED) fun createUserActivity( `@Valid` `@RequestBody` request: CreateUserActivityRequest ) = createUserActivityUseCase.execute(request)#!/bin/bash # 생성 엔드포인트의 상태코드 명시 여부 점검 rg -n -C2 '@PostMapping|@ResponseStatus|ResponseEntity' finda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/in/activity/ActivityWebAdapter.kt🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@finda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/in/activity/ActivityWebAdapter.kt` around lines 16 - 21, The createUserActivity handler in ActivityWebAdapter currently returns the use-case result without specifying a 201 status; update the controller to explicitly return HTTP 201 for resource creation by either adding `@ResponseStatus`(HttpStatus.CREATED) to the createUserActivity function or changing it to return a ResponseEntity (e.g., ResponseEntity.created(...).body(...)) that wraps createUserActivityUseCase.execute(request); ensure the chosen change is applied to the createUserActivity method signature so callers receive a CREATED response.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In
`@finda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/in/activity/ActivityWebAdapter.kt`:
- Around line 16-21: The createUserActivity handler in ActivityWebAdapter
currently returns the use-case result without specifying a 201 status; update
the controller to explicitly return HTTP 201 for resource creation by either
adding `@ResponseStatus`(HttpStatus.CREATED) to the createUserActivity function or
changing it to return a ResponseEntity (e.g.,
ResponseEntity.created(...).body(...)) that wraps
createUserActivityUseCase.execute(request); ensure the chosen change is applied
to the createUserActivity method signature so callers receive a CREATED
response.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 7e1249d2-301e-4aee-bef3-387012561c93
📒 Files selected for processing (5)
finda-volunteer/build.gradle.ktsfinda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/in/activity/ActivityWebAdapter.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/in/activity/dto/request/CreateUserActivityRequest.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/in/volunteer/VolunteerWebAdapter.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/out/grpc/UserGrpcAdapter.kt
✅ Files skipped from review due to trivial changes (1)
- finda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/in/activity/dto/request/CreateUserActivityRequest.kt
🚧 Files skipped from review as they are similar to previous changes (3)
- finda-volunteer/build.gradle.kts
- finda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/out/grpc/UserGrpcAdapter.kt
- finda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/in/volunteer/VolunteerWebAdapter.kt
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
finda-volunteer/src/main/kotlin/finda/findavolunteer/application/service/volunteer/CreateVolunteerService.kt (1)
117-119: TODO 항목 추적을 위한 이슈 생성을 권장합니다.Outbox 패턴과 Debezium 관련 TODO가 3개 있습니다. 이러한 작업들이 추적되고 있는지 확인해 주세요.
이 TODO 항목들을 추적하기 위한 GitHub 이슈를 생성해 드릴까요?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@finda-volunteer/src/main/kotlin/finda/findavolunteer/application/service/volunteer/CreateVolunteerService.kt` around lines 117 - 119, The three TODO comments in CreateVolunteerService (the outbox-related TODOs and the Debezium TODO) need tracked GitHub issues: create separate issues for "outbox: send activity hours & recruitment period", "outbox: send activity days & remind time", and "Debezium: read multiple tables to publish events", then update each TODO in CreateVolunteerService.kt to reference the new issue ID or URL (e.g., TODO(issue#123) or TODO: see https://.../issues/123) and add brief acceptance criteria and owner in the issue; ensure the issues include required work (design, implementation, tests, and migration/backfill if needed) and assign/milestone labels so they are discoverable.finda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/in/volunteer/mapper/CreateVolunteerRequestMapper.kt (1)
9-9: CreateVolunteerRequest의personnal필드명을personnel로 수정하는 것을 권장합니다.
personnal은personnel의 오타이며, 매퍼에서 올바르게 변환하고 있습니다. 다만 API 호출자와의 계약이 정해져 있다면 breaking change가 될 수 있으므로, API 버전 관리 계획을 고려하여 수정하세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@finda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/in/volunteer/mapper/CreateVolunteerRequestMapper.kt` at line 9, The CreateVolunteerRequest data class has a typo: the field is named `personnal` but should be `personnel`; update the field name in CreateVolunteerRequest to `personnel`, then update the mapper in CreateVolunteerRequestMapper (where `personnel = personnal`) to use the corrected name, and search/replace other references/usages of `personnal` (constructors, JSON serialization names, tests, API docs) so everything compiles; if this is a public API, add migration/versioning notes or keep a backward-compatible JSON alias (e.g., `@JsonProperty`("personnal") -> personnel) to avoid breaking clients.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@finda-volunteer/src/main/kotlin/finda/findavolunteer/application/service/volunteer/VolunteerDetailService.kt`:
- Around line 48-66: The code calls userQueryPort.getUserName repeatedly for the
same userId while building StudentParticipationResult and UserActivityResult;
instead, collect all distinct userIds from volunteerDetail.studentParticipations
and volunteerDetail.userActivities, call userQueryPort.getUserName once per
distinct id to build a Map<userId, userName>, then replace direct calls in the
StudentParticipationResult and UserActivityResult constructors to lookup the
name from that map and throw UserNotFoundException if a required id is missing;
update references to volunteerDetail.studentParticipations,
volunteerDetail.userActivities, StudentParticipationResult, UserActivityResult,
and userQueryPort.getUserName accordingly.
---
Nitpick comments:
In
`@finda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/in/volunteer/mapper/CreateVolunteerRequestMapper.kt`:
- Line 9: The CreateVolunteerRequest data class has a typo: the field is named
`personnal` but should be `personnel`; update the field name in
CreateVolunteerRequest to `personnel`, then update the mapper in
CreateVolunteerRequestMapper (where `personnel = personnal`) to use the
corrected name, and search/replace other references/usages of `personnal`
(constructors, JSON serialization names, tests, API docs) so everything
compiles; if this is a public API, add migration/versioning notes or keep a
backward-compatible JSON alias (e.g., `@JsonProperty`("personnal") -> personnel)
to avoid breaking clients.
In
`@finda-volunteer/src/main/kotlin/finda/findavolunteer/application/service/volunteer/CreateVolunteerService.kt`:
- Around line 117-119: The three TODO comments in CreateVolunteerService (the
outbox-related TODOs and the Debezium TODO) need tracked GitHub issues: create
separate issues for "outbox: send activity hours & recruitment period", "outbox:
send activity days & remind time", and "Debezium: read multiple tables to
publish events", then update each TODO in CreateVolunteerService.kt to reference
the new issue ID or URL (e.g., TODO(issue#123) or TODO: see
https://.../issues/123) and add brief acceptance criteria and owner in the
issue; ensure the issues include required work (design, implementation, tests,
and migration/backfill if needed) and assign/milestone labels so they are
discoverable.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 0d28a508-b5f9-49d9-bdb2-7ff6533b85c2
📒 Files selected for processing (17)
finda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/in/activity/ActivityWebAdapter.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/in/activity/mapper/CreateUserActivityRequestMapper.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/in/volunteer/VolunteerWebAdapter.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/in/volunteer/mapper/CreateVolunteerRequestMapper.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/in/volunteer/mapper/VolunteerResultMapper.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/application/port/in/activity/CreateUserActivityUseCase.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/application/port/in/activity/dto/request/CreateUserActivityCommand.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/application/port/in/volunteer/CreateVolunteerUseCase.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/application/port/in/volunteer/VolunteerDetailUseCase.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/application/port/in/volunteer/VolunteerListUseCase.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/application/port/in/volunteer/dto/request/CreateVolunteerCommand.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/application/port/in/volunteer/dto/response/VolunteerDetailResult.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/application/port/in/volunteer/dto/response/VolunteerListResult.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/application/service/activity/CreateUserActivityService.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/application/service/volunteer/CreateVolunteerService.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/application/service/volunteer/VolunteerDetailService.ktfinda-volunteer/src/main/kotlin/finda/findavolunteer/application/service/volunteer/VolunteerListService.kt
✅ Files skipped from review due to trivial changes (6)
- finda-volunteer/src/main/kotlin/finda/findavolunteer/application/port/in/volunteer/VolunteerDetailUseCase.kt
- finda-volunteer/src/main/kotlin/finda/findavolunteer/application/port/in/volunteer/dto/response/VolunteerListResult.kt
- finda-volunteer/src/main/kotlin/finda/findavolunteer/application/port/in/activity/CreateUserActivityUseCase.kt
- finda-volunteer/src/main/kotlin/finda/findavolunteer/application/port/in/volunteer/VolunteerListUseCase.kt
- finda-volunteer/src/main/kotlin/finda/findavolunteer/application/port/in/activity/dto/request/CreateUserActivityCommand.kt
- finda-volunteer/src/main/kotlin/finda/findavolunteer/application/port/in/volunteer/dto/request/CreateVolunteerCommand.kt
🚧 Files skipped from review as they are similar to previous changes (4)
- finda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/in/activity/ActivityWebAdapter.kt
- finda-volunteer/src/main/kotlin/finda/findavolunteer/application/service/volunteer/VolunteerListService.kt
- finda-volunteer/src/main/kotlin/finda/findavolunteer/application/service/activity/CreateUserActivityService.kt
- finda-volunteer/src/main/kotlin/finda/findavolunteer/adapter/in/volunteer/VolunteerWebAdapter.kt
| studentParticipationResultList = volunteerDetail.studentParticipations.map { | ||
| StudentParticipationResult( | ||
| userId = it.userId, | ||
| name = userQueryPort.getUserName(it.userId) ?: throw UserNotFoundException, | ||
| status = it.status, | ||
| participatedAt = it.participatedAt | ||
| ) | ||
| }, | ||
| activityResultList = volunteerDetail.activities.map { | ||
| ActivityResult( | ||
| activityId = it.id, | ||
| activityName = it.activityName | ||
| ) | ||
| }, | ||
| userActivityResultList = volunteerDetail.userActivities.map { | ||
| UserActivityResult( | ||
| userId = it.userId, | ||
| userName = userQueryPort.getUserName(it.userId) ?: throw UserNotFoundException, | ||
| activityId = it.activityId |
There was a problem hiding this comment.
동일 사용자명 조회의 중복 외부 호출을 줄여야 합니다.
Line 48-66에서 userQueryPort.getUserName을 항목별로 직접 호출하면, 동일 userId에 대해 gRPC 호출이 반복되어 응답 지연과 실패 가능성이 커집니다. 사용자 ID를 먼저 dedupe 후 한 번씩만 조회해 재사용하는 방식이 안전합니다.
♻️ 제안 수정안
@@
override fun execute(volunteerId: UUID): VolunteerDetailResult {
val volunteerDetail = volunteerDetailQueryPort.findDetailByIdOrThrow(volunteerId)
val volunteer = volunteerDetail.volunteer
+ val userNameById = (
+ volunteerDetail.studentParticipations.map { it.userId } +
+ volunteerDetail.userActivities.map { it.userId }
+ )
+ .distinct()
+ .associateWith { userId ->
+ userQueryPort.getUserName(userId) ?: throw UserNotFoundException
+ }
return VolunteerDetailResult(
@@
studentParticipationResultList = volunteerDetail.studentParticipations.map {
StudentParticipationResult(
userId = it.userId,
- name = userQueryPort.getUserName(it.userId) ?: throw UserNotFoundException,
+ name = userNameById.getValue(it.userId),
status = it.status,
participatedAt = it.participatedAt
)
},
@@
userActivityResultList = volunteerDetail.userActivities.map {
UserActivityResult(
userId = it.userId,
- userName = userQueryPort.getUserName(it.userId) ?: throw UserNotFoundException,
+ userName = userNameById.getValue(it.userId),
activityId = it.activityId
)
}
)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| studentParticipationResultList = volunteerDetail.studentParticipations.map { | |
| StudentParticipationResult( | |
| userId = it.userId, | |
| name = userQueryPort.getUserName(it.userId) ?: throw UserNotFoundException, | |
| status = it.status, | |
| participatedAt = it.participatedAt | |
| ) | |
| }, | |
| activityResultList = volunteerDetail.activities.map { | |
| ActivityResult( | |
| activityId = it.id, | |
| activityName = it.activityName | |
| ) | |
| }, | |
| userActivityResultList = volunteerDetail.userActivities.map { | |
| UserActivityResult( | |
| userId = it.userId, | |
| userName = userQueryPort.getUserName(it.userId) ?: throw UserNotFoundException, | |
| activityId = it.activityId | |
| val userNameById = ( | |
| volunteerDetail.studentParticipations.map { it.userId } + | |
| volunteerDetail.userActivities.map { it.userId } | |
| ) | |
| .distinct() | |
| .associateWith { userId -> | |
| userQueryPort.getUserName(userId) ?: throw UserNotFoundException | |
| } | |
| return VolunteerDetailResult( | |
| studentParticipationResultList = volunteerDetail.studentParticipations.map { | |
| StudentParticipationResult( | |
| userId = it.userId, | |
| name = userNameById.getValue(it.userId), | |
| status = it.status, | |
| participatedAt = it.participatedAt | |
| ) | |
| }, | |
| activityResultList = volunteerDetail.activities.map { | |
| ActivityResult( | |
| activityId = it.id, | |
| activityName = it.activityName | |
| ) | |
| }, | |
| userActivityResultList = volunteerDetail.userActivities.map { | |
| UserActivityResult( | |
| userId = it.userId, | |
| userName = userNameById.getValue(it.userId), | |
| activityId = it.activityId |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@finda-volunteer/src/main/kotlin/finda/findavolunteer/application/service/volunteer/VolunteerDetailService.kt`
around lines 48 - 66, The code calls userQueryPort.getUserName repeatedly for
the same userId while building StudentParticipationResult and
UserActivityResult; instead, collect all distinct userIds from
volunteerDetail.studentParticipations and volunteerDetail.userActivities, call
userQueryPort.getUserName once per distinct id to build a Map<userId, userName>,
then replace direct calls in the StudentParticipationResult and
UserActivityResult constructors to lookup the name from that map and throw
UserNotFoundException if a required id is missing; update references to
volunteerDetail.studentParticipations, volunteerDetail.userActivities,
StudentParticipationResult, UserActivityResult, and userQueryPort.getUserName
accordingly.
✨ 리뷰 시 참고 사항을 작성해주세요
👩💻 작업한 내용을 설명해주세요
📸 결과물을 캡쳐해주세요
🔗 관련 이슈 번호를 첨부하여주세요
Summary by CodeRabbit
새로운 기능
Chores