v0.2.11 릴리스 — 콘텐츠 도메인과 www 홈 개편 - #491
Merged
Merged
Conversation
#475 feat(mcp): 운영 도구 15종을 더해 업무·하위 업무·회의를 대화로 처리한다 MCP를 어드민 기능 전체로 넓히는 첫 파도(ssccops#365 W1 · ADR-0037). WorkTools 8종(목록·등록· 수정·하위 업무 등록·수정·투표·점검 항목 추가·문구 수정)과 MeetingTools 7종(회의 등록·전이·안건 목록·안건 추가·승인함·대시보드·유형 목록)이며 전부 McpRestClient로 자기 REST를 부른다. **수정은 읽고-합치기다** — 서버 PATCH가 전체 교체라 부분 호출이 다른 필드를 지우는 것이 1차에서 update를 뺀 이유였다(F2). WorkPatch·SubWorkPatch가 바꿀 것만 받고 상세를 GET해 빈 자리를 채운다. 도구용 DTO를 쓰지 않는다는 규칙의 예외라 McpPatchContractTest가 필드 1:1 대응과 nullable을 반사로 검사하고, 통합 테스트가 «priority만 준 호출이 제목·총평을 남기는지»를 REST 왕복으로 본다. 삭제 도구는 넣지 않았다 — 소프트 삭제만 열기로 했고(ADR-0037) W5에서 낸다. Claude-Session: https://claude.ai/code/session_01DLr6F8ATNB7cxHcGQTAK6d Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
… 건너뜀 (#478) #477 cicd(sonar): Analyze job이 Quality Gate 결과를 Pass/Fail로 말한다 — 주석·종료 코드·토큰 없음은 건너뜀 게이트 ERROR면 sonar-report.sh가 조건마다 ::error 주석을 내고 1로 끝나 job이 빨갛다. 그전에는 게이트가 무엇이든 초록이었고 develop이 세 번 연속 ERROR인 동안 아무도 몰랐다. 분석은 develop 푸시에서만 돌므로 막는 머지는 없다(ADR-0018 그대로). 토큰이 없을 때는 sonar-preflight job이 판정해 Analyze를 통째로 건너뛴다(회색) — secrets는 job if에서 못 읽어 output으로 넘긴다. 예전엔 step마다 가드가 붙어 step은 건너뛰고 job은 초록이었다. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* #479 feat(content): 콘텐츠 테이블 넷·CONTENT_MANAGE 시드·file_rfrnc CONTENT_POST 대상 V15가 cntnt_page·cntnt_page_hstry·cntnt_post·cntnt_post_hstry를 만들고 file_rfrnc의 trgt_se_cd CHECK를 CONTENT_POST까지 넓힌다(V13의 «enum이 늘면 제약도 함께»가 처음 정상 적용된 자리). V16이 CONTENT_MANAGE를 SUPER 직속으로 시드하고 회장·부회장에 부여한다 — 홍보국장·홍보국원 역할은 V3 시드에 없어 배포 뒤 역할별 권한 화면에서 켠다(#65). 페이지와 포스트를 한 테이블로 합치지 않은 것은 분류·활동일·요약·표지·갤러리가 포스트에만 있어 합치면 페이지 행의 절반이 NULL이기 때문이고(form_tmpl을 나눈 #142와 같은 판단), 이력을 bfr_/aftr_ 쌍이 아니라 전체 스냅샷으로 둔 것은 개인정보 처리방침 같은 페이지의 «그때 무엇이 게시돼 있었나»가 법적으로 필요한 값이라서다. 갤러리는 새 테이블이 아니라 file_rfrnc다 — #220이 그 테이블을 도메인 중립으로 연 이유가 이것이다. 대상당 여러 건이 정상인 첫 대상이라 FileReferenceRepository·Service에 목록·한 건 조회·추가·한 건 삭제가 생겼다(단건 findByTarget을 갤러리에 쓰면 둘째 행부터 조용히 사라진다). test 프로필은 Flyway가 꺼져 있어 data-locations·SeedScript.LOCATIONS에 V16을 함께 적었다 — 빠뜨리면 AuthorityFixture가 «data.sql이 넣어야 할 권한이 없다»로 죽는다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * #479 feat(content): 페이지·포스트 관리 API와 익명 콘텐츠 셋 — 이력·게시 감사·from-event·갤러리 domain/content — 어드민(/v1/content/pages·posts, 클래스 레벨 CONTENT_MANAGE)은 생성·수정(PATCH는 통째로 교체 · F2)·게시·게시 취소·이력·목록(커서)이고, 바꾸는 조작마다 이력이 한 행 늘며 게시·게시 취소만 AuditAction에 남긴다(본문 값 없음 · ADR-0024). 본문은 마크다운 원문 그대로이고 서버는 길이(10만 자 · 413)만 본다 — 파싱·raw HTML 차단은 www 렌더러의 몫이다(ADR-0038). from-event는 행사의 제목·시작일·장소·일시·본문을 복사한 초안을 만들 뿐 연결이 아니다 — EventRepository를 한 방향으로만 읽는다(DomainCycleTest). 갤러리는 presigned PUT 발급 순간 file_rfrnc(CONTENT_POST) 행이 생기고 fileId가 곧 표지 지정·삭제·익명 읽기 주소의 열쇠다 — 행사 이미지와 달리 열쇠가 파일명이 아니라 행이라 지운 장이 주소로 계속 열리지 않는다. 익명(/public/v1/pages/{slug} · /posts · /posts/{slug} · /posts/{postId}/images/{fileId})은 게시본만이고 초안과 없는 것이 같은 404다(게시 상태가 질의 조건). 응답은 어드민과 다른 Public* record이며 PublicContentDtoContractTest가 컴포넌트를 금지 목록과 대조한다. Cache-Control: public, s-maxage=300, stale-while-revalidate=600은 global/apipayload/ PublicCacheControl 한 곳 — 폼의 forms/open도 같은 값이라 content ↔ form 순환을 피해 global이다. GET /public/v1/forms/open은 폼 도메인(PublicFormMetaController)에 있다 — «접수 중»의 어휘가 FormReceiptPolicy에 있어서다. 시스템 폼(기획안)은 뺀다(익명 홈에 뜨면 로그인 벽 링크가 된다). SecurityConfig의 /public/v1 주석과 AGENTS(루트 도메인 표·마이그레이션 표·/public/v1 bullet · file·form·content)에 셋과 ADR-0038을 적었다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * #479 feat(mcp): 콘텐츠 도구 여섯 — create/update_page·create/update_post·publish_content·request_content_image_upload WorkTools·MeetingTools와 같은 규칙이다 — REST 자기 호출(McpRestClient) · 입력·출력은 컨트롤러 record 그대로 · 로그는 도구 이름과 대상 id만. update_page·update_post는 서버 PATCH가 통째로 교체라 ContentPagePatch·ContentPostPatch로 읽고-합치기 하며 McpPatchContractTest가 저장 요청 record와의 1:1을 본다(요약·행사·표지를 비우는 것은 MCP로 할 수 없다 — WorkPatch와 같은 한계). publish_content 하나가 kind=page|post + publish 불리언을 받는 것은 «게시»가 한 동작이라서다 — 넷으로 쪼개면 모델이 고를 것만 는다. 그래서 출력이 Object(kind에 따라 ContentPageResponse / ContentPostResponse)이고 도구용 합집합 record는 두지 않았다. request_content_image_upload는 presigned PUT URL과 fileId를 돌려줄 뿐 바이트를 올리지 않는다 — 서버가 바이트를 받지 않는 구조(#107)는 MCP에서도 같다. 도구 목록 테스트(advertisesEveryOperationTool)는 24 → 30이고 AGENTS «MCP» 절이 네 클래스를 말한다. ContentToolsIntegrationTest가 create_page → update_page → publish_content 왕복과 역할 없는 회원의 «권한 없음»을 본다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * #479 test(member): 권한 트리 테스트가 SUPER 직속 다섯째 노드 CONTENT_MANAGE를 본다 V16이 SUPER 아래에 한 노드를 더했으므로 시드 트리를 못 박는 AuthorityControllerTest의 자식 수와 순번을 맞춘다 — 전체 build에서만 드러난 실패다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
#481 fix(content): 익명 포스트 목록 커서 500 — 날짜 파라미터의 is null 검사를 질의 분리로 두 번째 장부터 PostgreSQL이 «could not determine data type of parameter $4»로 거절했다 — 홀로 선 «? is null»은 타입을 정할 문맥이 없고 LocalDate는 드라이버가 타입 없이 보낸다. H2 테스트는 통과해 못 잡았다(dev Coolify 로그 실측). 첫 장·커서 뒤 질의를 가른다. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* #483 feat(academicprogram): 스터디장이 모집 시작 일시 전까지 모집 폼 문항을 고친다 스터디장·프로젝트장 역할에는 권한이 하나도 없어(V3 시드) FORM_READ·FORM_WRITE로 잠긴 폼 경로에 닿지 못했고, 그래서 지원서 문항을 실제로 채우는 사람이 리더가 아니라 학술국장이었다. START_RECRUITMENT이 문항 0개를 400으로 막으므로 리더가 못 채우면 모집 자체가 열리지 않았다. - GET·PUT /v1/academic-programs/{id}/recruitment/form — 자격은 신청자 조회와 같은 requireLeaderOrManager다. 정적 권한 코드를 새로 만들지 않은 것은 「이 활동 한정 관계」가 역할로 표현되지 않기 때문이다(학술관리_API설계.md §4). - 창의 기준은 활동 상태가 아니라 form.rcpt_bgng_dt다 — 학술국장이 모집 관리에서 등록하는 그 일시이며, 판정은 FormReceiptPolicy가 준 DRAFT·SCHEDULED 하나다. 활동 상태로 보면 미래 시작일 모집에서 창이 곧바로 닫혀, 화면이 남은 시간을 세면서 저장은 409가 된다. 조회는 창과 무관하게 200이고 isEditable만 false다. - 요청 본문은 문항 구성 하나다 — 접수 기간·제목·라벨·다중 응답은 받지 않는다. 받지 않으면 덮어쓸 수 없고, 그것이 「모집 일정은 학술국장이 정한다」의 두 번째 방어선이다(첫 번째는 #190의 ACADEMIC_FORM_RECEIPT_PERIOD_LOCKED). - 저장 규칙은 폼 도메인이 갖는다 — FormService.changeQuestionComposition은 changeReceiptPeriod와 같은 자리이며 updateForm과 같은 검증·이력 경로를 지난다 (공통 조각을 ensureQuestionCompositionReplaceable로 뽑았다). - 목록이 모집 카드 값을 싣는다 — formId·formReceiptStatus·접수 기간·qitemVer· 정원·applicationCount·approvedAt. 질의는 left join fetch 한 줄과 접수 건수 집계 한 벌이고, 세는 기준은 폼 목록의 responseCount와 같다. 새 테이블·컬럼·권한이 없어 마이그레이션도 없다. * #483 test(academicprogram): 리더는 자기가 맡은 활동의 모집 폼만 고친다는 것을 못 박는다 기존 403 테스트는 국원(다른 권한만)과 팀원뿐이라 «리더 A가 활동 B를 고치려 할 때»가 비어 있었다 — 요구 2가 바로 그 자리다. 판정이 leadr_mbr_id 비교라 통과하고 있었지만 회귀를 막을 것이 없었다. 픽스처의 폼은 leader가 만든 것인데도 403이다 — 자격의 축이 form.creatr_mbr_id가 아니라 acdm_actv.leadr_mbr_id임을 함께 드러낸다.
#485 feat(academic-program): 모집 시작 뒤에도 접수 기간을 고치는 전용 경로 모집 시작(START_RECRUITMENT)은 APPROVED에서만 일어나 이미 모집 중인 활동의 날짜를 고칠 수 없었고, 폼 편집은 #190이 400 ACADEMIC_FORM_RECEIPT_PERIOD_LOCKED로 잠갔다. 세 경로가 모두 막힌 채 웹의 안내만 «모집 관리에서 설정합니다»를 가리키고 있었다. - GET .../recruitment/schedule — 접수 기간 + 파생 접수 상태. 리더 본인 또는 학술국장 - PATCH .../recruitment/schedule — 학술국장 전용. 모집 시작 전은 409 전이가 아니라 PATCH인 것은 활동 상태를 바꾸지 않기 때문이다. 저장은 기존 FormService.changeReceiptPeriod를 그대로 불러 기간 역전 검증이 폼 도메인 한 곳에 남는다. 두 필드는 전체 교체이고 null은 «제한 없음»이다. 근거: SoongSilComputingClub/ssccops#393
…() 함수(V17) - `global/db/DatabaseKeepAlive` — 04:00 KST + 기동 직후 `SELECT 1`, 실패는 ERROR 로그. `ssccops.db.keepalive.enabled`(test만 false) - `global/config/SchedulingConfig` — `@EnableScheduling`(둘째 스케줄 작업이라 문을 연다) - `V17__create_heartbeat_function.sql` — `public.heartbeat()` + anon 역할이 있을 때만 EXECUTE grant + `NOTIFY pgrst` - AGENTS: «앱 DB는 Supabase Free다 — 일시정지와 anon 권한» 절 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…-keepalive [#487] Supabase 일시정지 방지 1차 — 하루 한 번 SELECT 1 스케줄러 + heartbeat() 함수(V17)
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.
v0.2.11 릴리스. 상위 SoongSilComputingClub/ssccops#400
들어가는 것
배포 시 주의
Flyway 마이그레이션 3개가 돈다.
V15__create_content_tables.sql— 콘텐츠 페이지·포스트·이력 테이블V16__seed_content_manage_authority.sql— 콘텐츠 관리 권한 시드V17__create_heartbeat_function.sql—public.heartbeat()+ anon EXECUTE (ADR-0041)새 env 변수 없음.
SSCCOPS_DB_KEEPALIVE_ENABLED가 새로 생겼지만 기본값true라 Coolify에 넣지 않아도 켜진다 — 끌 때만 넣는다.MCP는 도구 추가만이고 인증 설정은 그대로다.
SecurityConfig변경은 주석뿐이며 새permitAll이 없다. prod/.well-known/oauth-protected-resource는 200으로 확인했다.이 PR을 웹보다 먼저 머지한다 — 웹이 새 콘텐츠 API를 부른다.
머지 방식
regular merge (squash 금지) — squash하면 develop 전체가 main 커밋 하나로 뭉개진다.