Repository navigation
[Feat/#142] 게시판(공지) 관리자 API 보완 - 고정 여부 반영 및 동시 수정 제어 - #143
Merged
Merged
Conversation
관리자 웹의 공지 폼이 고정 여부를 등록·수정과 함께 보내고, 수정 화면이 기존 공지와 수정 시각을 불러와야 해서 맞춘다.
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configuration
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 |
github-actions
Bot
requested review from
leegain1,
sangrae2325,
tnals0924 and
xeoxxn
October 18, 2026 16:24
This was referenced Oct 18, 2026
jjunh33
added this pull request to stack #147
October 18, 2026 17:32
tnals0924
approved these changes
Oct 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
#️⃣연관된 이슈
🎯 해결하려는 문제가 무엇인가요?
관리자 웹(
stream-admin-web)의 공지 화면과 공지 관리자 API(#104)가 맞지 않습니다.pinned를 읽지 않으며 고정은PATCH /{noticeId}/pin으로만 바꿉니다. 지금 연결하면 스위치가 오류 없이 무시됩니다.pinned가 없어 값을 얻을 방법이 없습니다.updatedAt을 요구해서 수정 화면이 이를 들고 있다가 되돌려 보내야 합니다. 관리자가 몇 명뿐인 공지 수정에서는 "원래 글이 그 사이 바뀌었는지"보다 "수정한 내용이 반영되는 것"이 중요한데, 이 검사가 폼과 API를 복잡하게 만듭니다.❓ 왜 해결해야 하나요?
관리자 웹이 공지 API를 연동하려면
pinned반영, 목록 조회, 수정 화면용 상세 조회가 먼저 있어야 합니다. 연동 전에 요청·응답 계약을 확정해 두면 이후 변경 비용이 없습니다.⭐ 어떻게 해결했나요?
pinned를 작성·수정 요청에 추가(필수).NoticeCreateCommand·NoticeUpdateCommand·Notice가 함께 받습니다. 고정은 폼의 스위치로만 바뀌고 관리자 웹 목록에 고정 토글이 없어서PATCH /{noticeId}/pin은 삭제했습니다.GET /v1/admin/notices:category(GENERAL/PARTNERSHIP, 종류 필터),keyword(제목 검색),page(0부터),size(1~100, 기본 20).category와keyword는 선택이고 함께 쓸 수 있으며,category가 잘못된 값이면 400입니다(학생용 목록과 같은 변환). 고정 우선, 최신순이고 항목은noticeId,title,category,pinned,createdAt이며 전체 개수와 페이지 정보를 함께 내려줍니다(오프셋 방식,PageParams/PageResponse). 검색은n.title LIKE CONCAT('%', :keyword, '%') ESCAPE '!'로 제목에 검색어가 포함된 공지를 찾습니다. 공백 검색어는 서비스가 조건 없음으로 취급하고(AdminNoticeServiceImpl.search), 저장소 구현이 검색어의%·_·!를LikePatterns.escape로 이스케이프해서 넘기므로 이 문자들도 글자 그대로 검색됩니다. 종류 조건은 데이터 쿼리와countQuery양쪽에 넣어 목록과 전체 개수가 같은 필터를 따릅니다. 목록과 전체 개수 두 문장을 같은 스냅샷에서 읽도록 읽기 전용 트랜잭션으로 묶었습니다(저장소의@Query메서드에는 트랜잭션이 붙지 않습니다).STUDENT권한을 가지므로GET /v1/app/notices/{noticeId}를 그대로 씁니다.NoticeDetailResponse에pinned를 추가했고 이미지·첨부의fileId는 이미 내려줍니다. 학생용 목록은 바꾸지 않았습니다.updatedAt,NoticeUpdateCommand.expectedUpdatedAt, 서버의 비교(validateNotModified), 에러 코드NOTICE_UPDATE_CONFLICT를 없앴습니다. 두 관리자가 겹쳐 수정하면 나중에 저장한 내용이 반영됩니다.NoticeJpaEntity에@DynamicUpdate: 바뀐 컬럼만 UPDATE합니다. 수정과 삭제가 겹쳤을 때 읽어 둔 나머지 컬럼까지 되써서 삭제된 공지가 되살아나거나(is_deleted) 다른 관리자가 방금 고친 값이 되돌아가는 일을 막습니다. 락이 아니라 UPDATE 범위만 줄입니다.TextArea로 입력받고 학생 웹은 텍스트로 렌더링하므로 요청@Schema의 "본문(HTML)"을 "일반 텍스트"로 고쳤습니다. 서버 동작은 그대로입니다.NoticeServiceImpl.getNotices/getNotice는 SELECT 한 문장뿐이라 트랜잭션이 더하는 것이 없어서@Transactional(readOnly = true)를 뺐습니다(한 문장은 트랜잭션 없이도 일관되게 읽힙니다). 쿼리가 둘 이상으로 늘면readOnly를 다시 붙입니다.🧩 이 PR의 한계 & 트레이드오프
LIKE '%키워드%'는 앞에 와일드카드가 있어 삭제되지 않은 행을 훑습니다. 공지 수가 많지 않은 규모를 가정했고, 늘면 FULLTEXT(ngram)를 검토해야 합니다.!입니다. 참고한 구현은\를 쓰지만 MySQL은 문자열 리터럴에서\를 이스케이프로 먹어ESCAPE '\'를 쓰려면 SQL을'\\'로 적어야 하고, JPQL을 거치면 이 이중 이스케이프가 어긋나기 쉬워서!를 골랐습니다. 쿼리의ESCAPE '!'와LikePatterns.ESCAPE가 같아야 합니다.LIKE ... ESCAPE '!'는 표준 문법이라 위험이 낮지만 MySQL에서 실행한 것은 아닙니다. 로컬에 Docker가 없어 H2 임시 테스트(커밋하지 않음)로 오프셋 페이지, 전체 개수, 키워드 검색(LIKE+ 이스케이프:%·_·!가 글자로 검색됨, 앞뒤 공백 제거), 종류 필터(키워드와 조합, 페이지를 넘길 때 개수 반영)까지만 확인했습니다. 쿼리가 틀리면 앱 기동이 실패하므로 dev 배포 후 가장 먼저 확인이 필요합니다.noticeId만 줍니다. 저장 뒤 화면 갱신이 필요하면 상세를 다시 조회하면 됩니다.@DynamicUpdate동작만 확인했고 MySQL에서는 확인하지 못했습니다. 확인한 것: 바뀐 컬럼만 SET에 붙는 것, 같은 컬럼 동시 수정은 나중 저장이 반영되는 것, 수정이 삭제된 공지를 되살리지 않는 것, 삭제가 동시 수정을 덮어쓰지 않는 것.⛓️ 기존 기능에 미치는 영향
pinned가 필수로 추가되고, 수정 요청의updatedAt이 사라지며(보내도 무시됩니다),PATCH /v1/admin/notices/{noticeId}/pin과409 NOTICE_UPDATE_CONFLICT가 사라집니다. [Feat/#103] 게시판(공지) 관리자 API 추가 #104 이후 이 API를 호출하는 클라이언트는 없습니다(관리자 웹은 아직 목업입니다).pinned필드가 추가됩니다(하위 호환). 학생용 목록은 그대로입니다.NoticeService·NoticeRepository에search가 추가됩니다(관리자 목록 전용,AdminNoticeService로 이동은 [Refactor/#144] 공지 관리자 기능을 AdminNoticeService로 분리 #145).🔀 Edge Case & 실패 시나리오
pinned누락INVALID_INPUT("고정 여부를 입력해 주세요.")NOTICE_NOT_FOUNDsize가 1~100 밖 /page음수INVALID_INPUTcategory가 없음category가GENERAL/PARTNERSHIP이 아님INVALID_INPUTcategory와keyword를 함께 지정keyword가 비었거나 공백keyword가%,_,!등을 포함%는 제목에%가 든 공지만)NOTICE_NOT_FOUND📋 검토한 대안과 선택 이유
updatedAt비교 + 낙관적 락): 이 PR의 이전 커밋에서 구현했으나, 수정 요청 시점에는 원래 글이 바뀌었는지보다 수정 내용이 반영되는 것이 중요하다고 판단해 제거했습니다. 폼과 API의updatedAt왕복, 409 처리, 예외 변환, MySQL 검증이 필요한 부분(JSON 컬럼 비교, 콜레이션)이 모두 사라집니다. 화면 단위 보호가 필요해지면@Version을 도입할 수 있습니다.pinned를 선택값으로 받기: 수정이 전체 교체(PUT)라 누락이 의도 불명확한 해제가 될 수 있어서 필수로 했습니다.keyword를 붙여 공용으로: 이 PR의 중간 커밋에서 구현했으나, 관리자 화면에는 페이지 번호 이동과 전체 개수가 필요해서 되돌리고 오프셋 방식의 관리자 전용 목록으로 만들었습니다.pinned하나만 더하면 같은 일을 해서 중복(응답·UseCase)을 줄이려 공용으로 했습니다.💬 리뷰 포인트
[r]동시 수정을 막지 않고 마지막 저장이 이기게 한 정책. PUT 전체 교체라 안 건드린 필드도 되돌아갈 수 있다는 점을 받아들일 수 있는지[r]NoticeJpaRepository.search의 JPQL(countQuery포함)이 dev에서 정상 기동·조회되는지[c]상세 조회를 학생 API로 공용화한 것[c]pinned를 필수로 둔 것과PATCH /pin삭제[a]본문@Schema문구