Skip to content

[project] 신청 행 단일화 및 수정 시 아이콘 유실 수정 - #452

Merged
ZaMan0806 merged 4 commits into
developfrom
fix/project-edit-request-single-row
Sep 25, 2026
Merged

ZaMan0806 merged 4 commits into
developfrom
fix/project-edit-request-single-row

Conversation

@ZaMan0806

@ZaMan0806 ZaMan0806 commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

개요

프로젝트 수정과 관련한 두 가지 문제를 해결했습니다. 수정 신청 행이 무한히 쌓이던 문제와, 수정 시 아이콘이 사라지던 문제입니다.

본문

1. 수정 신청 행 누적

수정 신청을 반복하면 REJECTED / ACCEPTED 행이 그대로 남아 누적됐습니다. 자신의 프로젝트를 한 번에 모두 조회하는 구조라, 수정·거절을 반복할수록 읽어야 할 행이 늘어납니다. 참여자·리포지토리·기술스택 컬렉션까지 행마다 따라붙어 증가폭이 큽니다.

재사용 대상을 전체 상태로 넓혀 프로젝트당 1행으로 고정했습니다.

val request = projectEditRequestJpaRepository
    .findByOriginalProjectId(projectId)
    .orElseGet { ProjectEditRequestJpaEntity().apply { originalProject = project } }

request.requestStatus = ProjectRequestStatus.PENDING
request.rejectReason = null
request.processedAt = null

재사용 방식이라 과거 거절 사유는 남지 않습니다. 다만 QueryMyProjectServiceImpl이 이미 프로젝트당 최신 1건만 사용하고 있어 화면 동작에는 변화가 없습니다.

신규 신청(original_project_id IS NULL)은 제한하지 않습니다. 초안에서는 신청자당 1행으로 묶었으나, 서로 다른 프로젝트를 동시에 신청할 때 기존 신청이 대체되는 부작용이 있어 리뷰 반영 과정에서 되돌렸습니다.

2. 수정 시 아이콘·배포 URL 유실

iconKey를 보내지 않으면 null로 덮어쓰고, 승인 시 applySnapshot이 이를 tb_project에 반영해 아이콘이 실제로 사라졌습니다. 응답에는 iconUrl만 있고 iconKey가 없어, 클라이언트가 되돌려 보낼 값을 알 수 없는 구조였습니다.

생략하면 유지, 빈 문자열이면 삭제로 규칙을 통일했습니다.

요청 값 동작
생략 / null 현재 값 유지
"" 삭제
값 있음 교체

기준값은 원본 프로젝트의 현재 값입니다. 신청 행을 기준으로 삼으면 신청 행이 없는 첫 수정에서 값이 비고, 그 사이 어드민이 원본을 바꿨을 때 과거 값으로 되돌아갑니다.

val currentProject = request.originalProject
request.iconKey = projectIconStorage.resolveIconKeyForUpdate(reqDto.iconKey, currentProject?.iconKey)
request.deploymentUrl = resolveDeploymentUrlForUpdate(reqDto.deploymentUrl, currentProject?.deploymentUrl)

적용 범위는 학생 신청 API, datagsm-web 어드민 수정 API, datagsm-openapi 수정 API 세 곳이며 deploymentUrl도 동일합니다. deploymentUrl은 @Pattern을 ^$|^https?://.*로 완화해 빈 문자열을 허용합니다.

응답에 iconKey를 추가해 클라이언트가 현재 키를 확인할 수 있게 했습니다. 공개 조회(PublicProjectResDto)는 무인증이고 쓰기 경로가 없어 제외했습니다.

검증

전체 테스트 800개 통과. 아래 케이스를 추가했습니다.

신청 행 단일화

  • 거절된 수정 신청을 재사용해 PENDING으로 되돌리고 거절 정보를 비우는지
  • 거절 건 재신청 시 새 행을 만들지 않는지
  • 승인된 수정 신청도 재사용해 한 행만 유지하는지
  • 신규 신청은 기존 신청을 건드리지 않고 새 행을 만드는지
  • 신규 신청에서 이전 신청의 값이 승계되지 않는지

아이콘·배포 URL 유지

  • 신청 행이 없는 첫 수정에서 원본 값이 승계되는지
  • 신청 행이 있어도 원본의 현재 값을 따르는지 (신청 행의 과거 값이 아님)
  • 빈 문자열을 보내면 삭제되는지
  • 새 값을 보내면 교체되는지
  • 어드민 수정 API에서도 동일한지

프론트 영향

  • 응답에 iconKey 필드가 추가됩니다 (MyProjectResDto, ProjectEditRequestResDto, ProjectResDto)
  • 수정 시 iconKey·deploymentUrl을 생략하면 기존 값이 유지됩니다
  • 입력을 비워 삭제하려면 ""를 보내면 됩니다

배포 시 주의

조회가 단건으로 바뀌어, 기존 DB에 같은 프로젝트를 가리키는 수정 신청 행이 여러 개면 NonUniqueResultException이 발생합니다. 운영에 데이터가 쌓여 있다면 배포 전 최신 1건만 남기는 정리가 필요합니다. 정리 SQL은 로컬 docs/db-migration.md에 정리해 두었습니다.

수정 신청을 반복할 때마다 tb_project_edit_request에 행이 계속
쌓여 신청자 한 명이 조회할 데이터가 기하급수적으로 늘어났다.

재사용 대상을 PENDING에서 전체 상태로 넓혀, 이전 상태와 무관하게
기존 행을 덮어쓰도록 바꿨다. 승인 전 신규 신청도 신청자당 하나만
유지한다. 거절 이력은 남지 않지만 조회 로직이 최신 1건만 사용하고
있어 실제 동작에는 영향이 없다.
@github-actions github-actions Bot added the waiting for review:검토 대기 확인을 대기하고 있습니다 label Sep 23, 2026

@pr-agent-demo pr-agent-demo Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview

  • 프로젝트 수정/신규 신청 로직을 변경해 프로젝트당 신청 행 1개, 승인 전 신규 신청은 신청자당 1개만 유지하도록 했습니다.
  • 기존 행을 상태와 무관하게 재사용·덮어쓰고, 관련 리포지토리 쿼리도 그에 맞게 조정했습니다.

Intent

  • 반복적인 신청/거절/재신청으로 tb_project_edit_request 행이 누적되는 문제를 방지하려는 의도로 보입니다.
  • 조회 성능 개선과 데이터 간결성 확보가 목적입니다.

Risk

  • 중간~높음
  • 기존 운영 DB에 중복된 신청 행이 남아 있으면 단건 조회 시 NonUniqueResultException이 발생할 수 있어, 배포 전 데이터 정리가 필요합니다.
  • 과거 거절 이력이 기존 행에 덮어써지는 구조로 바뀌어, 감사/이력 보존이 필요한 경우 별도 설계가 필요합니다.
  • 테스트는 추가되어 있지만, 운영 데이터 상태와 기존 화면/쿼리가 최신 1건 전제로 동작하는지 확인이 필요합니다.

0 inline comment(s)

iconKey를 보내지 않으면 null로 덮어써 승인 시 아이콘이 소실됐다.
응답에는 iconUrl만 있어 클라이언트가 되돌려 보낼 키를 알 수 없는
구조라, 이름만 고쳐도 아이콘이 사라졌다.

요청에 값이 없으면 기존 값을 유지하도록 바꾸고, 응답에 iconKey를
추가해 클라이언트가 현재 키를 확인할 수 있게 했다. 학생 신청과
어드민 수정 API 모두에 적용했으며 deploymentUrl도 동일하게 처리한다.
공개 조회 응답에는 내부 키를 노출하지 않는다.
@ZaMan0806 ZaMan0806 changed the title [project] 신청 행을 프로젝트당 하나로 고정 [project] 신청 행 단일화 및 수정 시 아이콘 유실 수정 Sep 23, 2026
hongjm0912
hongjm0912 previously approved these changes Sep 23, 2026
@yeondon125

Copy link
Copy Markdown
Member

클라이언트(projects·admin) 반영 관점에서 확인해 봤는데, 아이콘 유실이 아직 남는 경로가 있어 공유드립니다.

1. 어드민 수정 API(datagsm-web)는 아직 전체 덮어쓰기입니다

어드민 앱이 호출하는 PUT /v1/projects/{projectId}는 datagsm-web의 ModifyProjectServiceImpl인데, 이번 PR에서는 datagsm-openapi 쪽만 ?.let으로 바뀌었습니다. web 쪽은 그대로라 어드민에서 수정하면 아이콘·배포 URL이 지워집니다.

project.iconKey = projectIconStorage.validateIconKey(reqDto.iconKey)
project.deploymentUrl = reqDto.deploymentUrl

2. 학생의 첫 수정 신청에서는 아이콘이 여전히 사라집니다

  • 신규 신청을 수락해도 신청 행의 originalProject는 null로 남고, 어드민이 직접 만든 프로젝트는 신청 행이 아예 없습니다.
  • 그래서 첫 수정 신청은 findByOriginalProjectId가 비어 새 행(iconKey = null)이 만들어지고, iconKey를 생략하면 null인 채로 남습니다. 수락 시 applySnapshot이 이 값으로 원본을 덮어 아이콘이 사라집니다.
  • 추가된 테스트는 기존 신청 행이 있는 경우(Optional.of(previousRequest))만 다뤄서 이 경로가 빠져 있습니다.

.findByOriginalProjectId(projectId)
.orElseGet { ProjectEditRequestJpaEntity().apply { originalProject = project } }

기존 행이 있는 경우에도 유지되는 값이 원본 프로젝트의 현재 값이 아니라 직전 신청 행의 값입니다. 그 사이 어드민이 아이콘·배포 URL을 직접 바꿨다면, 다음 학생 수정이 수락될 때 이전 값으로 되돌아갑니다.

생략 시 기준을 원본 프로젝트로 두면 두 경우 모두 해결될 것 같습니다.

request.iconKey = reqDto.iconKey?.let { projectIconStorage.validateIconKey(it) } ?: request.originalProject?.iconKey
request.deploymentUrl = reqDto.deploymentUrl ?: request.originalProject?.deploymentUrl

신규 신청은 originalProject가 없으니 생략하면 null이 되어, 아래 3번의 값 승계도 같이 막힙니다.

3. 신규 신청이 신청자당 1행이라, 다른 프로젝트를 신청하면 기존 신청이 덮어써집니다

ApplyProjectServiceImpl이 신청자의 미승인 신규 신청(대기·거절)을 찾아 재사용하므로, A를 신청해 둔 상태에서 전혀 다른 프로젝트 B를 신청하면 A가 B로 대체됩니다. 게다가 B에 아이콘·배포 URL을 넣지 않으면 A의 값이 그대로 남아 B에 붙습니다.

.findByOriginalProjectIsNullAndRequestedByIdAndRequestStatusNot(
applicant.id!!,
ProjectRequestStatus.ACCEPTED,
).orElseGet { ProjectEditRequestJpaEntity().apply { originalProject = null } }

  • 학생이 두 프로젝트를 동시에 신청할 수 없는 게 의도된 정책인지 확인 부탁드립니다.
  • 의도라면 클라이언트에서 "기존 신청이 대체된다"는 안내를 넣겠습니다. 값 승계는 서버에서 막아야 합니다(2번 제안으로 함께 해결됩니다).

4. 배포 URL은 삭제할 방법이 없습니다

생략·null은 기존 값 유지, ""는 @Pattern("^https?://.*")에 걸려 400이라 한 번 넣은 배포 URL을 지울 수 없습니다.

반면 iconKey는 ""를 보내면 validateIconKey가 blank를 null로 돌려줘서 실제로 삭제됩니다. 본문의 "아이콘 삭제 수단 없음"과는 다르게 동작합니다.

reqDto.iconKey?.let { request.iconKey = projectIconStorage.validateIconKey(it) }
reqDto.deploymentUrl?.let { request.deploymentUrl = it }

두 필드 모두 "" = 삭제로 명시해 주시면 클라이언트는 "입력을 비우면 "" 전송"으로 맞추겠습니다. (배포 URL은 @Pattern을 ^$|^https?://.*로 완화하고 blank를 null로 저장하는 식)

기타

  • openapi GET /v1/projects 응답(ProjectResDto)에도 iconKey가 추가되어 API 키 사용자에게 내부 오브젝트 키가 노출됩니다. PublicProjectResDto에서 뺀 취지와 맞는지 확인 부탁드립니다.

클라이언트는 머지 후 수정 시 응답의 iconKey를 그대로 되돌려 보내도록 반영할 예정이라 화면상 유실은 막을 수 있습니다. 다만 다른 호출 경로도 있어서 서버 기본 동작도 맞춰 두는 게 안전할 것 같습니다.

리뷰에서 지적된 네 가지를 반영했다.

- datagsm-web의 어드민 수정 API가 누락되어 여전히 전체 덮어쓰기였다
- 신청 행이 없는 첫 수정과, 어드민이 원본을 직접 바꾼 경우를 위해
  기준을 신청 행이 아닌 원본 프로젝트의 현재 값으로 바꿨다
- 신규 신청을 신청자당 하나로 묶으면 다른 프로젝트를 신청할 때
  기존 신청이 대체되고 값까지 승계되므로 제한을 없앴다
- 생략은 유지, 빈 문자열은 삭제로 iconKey와 deploymentUrl 규칙을
  통일했다. deploymentUrl의 @pattern에 빈 문자열을 허용한다
@ZaMan0806
ZaMan0806 merged commit 019fedf into develop Sep 25, 2026
4 checks passed
@ZaMan0806
ZaMan0806 deleted the fix/project-edit-request-single-row branch September 25, 2026 06:30
@github-actions github-actions Bot removed the waiting for review:검토 대기 확인을 대기하고 있습니다 label Sep 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants