feat(ai-character): 수정 요청 변경 필드만 전송
This commit is contained in:
274
docs/20260806_수정요청변경필드만전송/prd.md
Normal file
274
docs/20260806_수정요청변경필드만전송/prd.md
Normal file
@@ -0,0 +1,274 @@
|
||||
# 수정 요청 변경 필드 전송 PRD
|
||||
|
||||
## 문서 정보
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 문서 상태 | 구현 기준 확정 |
|
||||
| 작성일 | 2026-08-06 |
|
||||
| 최종 수정일 | 2026-08-06 |
|
||||
| 대상 제품 | AI 캐릭터 관리자 웹 수정 요청 최적화 |
|
||||
| 작성자·결정권자 | 작성자: Codex / 결정권자: 사용자 |
|
||||
| 관련 API Contract | [api-contract.md](./api-contract.md) |
|
||||
| 관련 구현 계획 | [plan-task.md](./plan-task.md) |
|
||||
| 관련 review | 없음 — 구현 완료 후 `reviews/`에 추가 |
|
||||
|
||||
### 요구사항 상태
|
||||
|
||||
| 상태 | 의미 | 구현 처리 |
|
||||
|---|---|---|
|
||||
| 확정 | 제품·기술 결정이 완료되어 구현 기준으로 사용 | `plan-task.md`의 Task와 완료 증거로 추적 |
|
||||
| 미결 | 제품·UX·운영 결정이 더 필요함 | 권고안과 결정 주체·시점을 기록하고 임의 구현 금지 |
|
||||
| 외부 의존 | 프론트엔드 밖의 계약·권한·환경 제공이 필요함 | 담당 주체·영향·재개 조건을 기록하고 추정 구현 금지 |
|
||||
| 권고 | 미결 항목에 대한 현재 추천안 | 확정 전 계약이나 수용 기준으로 사용하지 않음 |
|
||||
| 제외 | 현재 범위에서 구현하지 않기로 결정 | 제외 이유와 후속 조건을 Decision Log에 기록 |
|
||||
|
||||
### 문서 우선순위와 갱신 순서
|
||||
|
||||
1. 사용자·제품 결정은 이 PRD에 기록한다.
|
||||
2. request payload 규칙은 [api-contract.md](./api-contract.md)에 기록한다.
|
||||
3. 구현 범위·순서·완료 증거는 [plan-task.md](./plan-task.md)에 기록한다.
|
||||
4. 요구사항 변경 시 Decision Log → 요구사항·수용 기준 → API Contract → 구현 계획 순서로 갱신한다.
|
||||
5. 기존 결정과 검증 기록은 삭제하거나 덮어쓰지 않고 정정 기록을 누적한다.
|
||||
|
||||
## 1. Overview
|
||||
|
||||
관리자가 AI 캐릭터, 오디오 콘텐츠, 커뮤니티 게시글, 시리즈, FanTalk 답글을 수정하면 프론트엔드는 현재 값 전체가 아니라 최종적으로 변경된 필드만 기존 수정 endpoint에 전송한다. 기존 화면, HTTP method, multipart 구조, 인증·응답·오류 계약은 유지하고 request payload 생성과 무변경 저장 동작만 바꾼다.
|
||||
|
||||
## 2. Problem Statement
|
||||
|
||||
현재 관리자는 다음 문제를 겪는다.
|
||||
|
||||
- 한 필드만 수정해도 화면이 보유한 다른 수정 가능 값이 함께 전송된다.
|
||||
- 사용자가 건드리지 않은 값까지 서버에 다시 기록될 수 있어 동시 변경을 덮어쓸 위험과 payload 확인 비용이 커진다.
|
||||
- 일부 화면은 이미 특정 필드만 생략하지만, 도메인마다 규칙이 달라 변경 필드 전송 여부를 일관되게 검증하기 어렵다.
|
||||
|
||||
문제를 해결했다는 판단은 각 수정 화면에서 한 필드만 바꿨을 때 request JSON에 그 필드만 존재하고, 변경이 없을 때 저장 버튼이 비활성화되며 mutation 요청이 0건인 것으로 한다.
|
||||
|
||||
## 3. Goals
|
||||
|
||||
### 3.1 제품 목표
|
||||
|
||||
- 다섯 수정 기능이 실제 변경 필드와 새로 선택한 파일만 전송한다.
|
||||
- 값 삭제는 기존 nullable 계약에 맞는 `null`을 전송하고, 미변경은 key 생략으로 구분한다.
|
||||
- 변경 후 원래 값으로 되돌리면 변경 없음으로 판정해 불필요한 mutation을 만들지 않는다.
|
||||
|
||||
### 3.2 UX 목표
|
||||
|
||||
- 변경 사항이 없으면 저장 버튼을 비활성화해 요청이 발생하지 않음을 사전에 알린다.
|
||||
- 유효한 변경이 있으면 기존 저장 중·성공·실패·중복 제출 방지 동작을 유지한다.
|
||||
- 기존 반응형 capability, keyboard 동작, label·오류 연결과 focus 정책을 회귀시키지 않는다.
|
||||
|
||||
## 4. Non-Goals
|
||||
|
||||
- 기존 `PUT` endpoint를 `PATCH`로 바꾸지 않는다.
|
||||
- backend DTO, response, 오류 status/key 또는 저장 로직을 변경하지 않는다.
|
||||
- 생성, 비활성화, 게시글 고정 전환, 시리즈 순서 변경, FanTalk 원글 삭제 payload는 변경하지 않는다.
|
||||
- 수정 화면에 없는 필드를 새로 노출하지 않는다.
|
||||
- 새 dependency, 범용 form library 또는 전역 diff framework를 도입하지 않는다.
|
||||
|
||||
Non-Goal 변경 시 Decision Log와 `plan-task.md` 범위를 먼저 갱신한다.
|
||||
|
||||
## 5. Target Users and Permissions
|
||||
|
||||
### 5.1 사용자
|
||||
|
||||
| 사용자 | 목표 | 주요 작업 | 사용 환경 |
|
||||
|---|---|---|---|
|
||||
| 인증된 관리자 | 선택한 리소스의 의도한 값만 안전하게 수정 | 캐릭터·오디오 콘텐츠·커뮤니티 게시글·시리즈·FanTalk 답글 수정 | 기존 기능별 지원 viewport |
|
||||
|
||||
### 5.2 권한
|
||||
|
||||
- 인증 주체: 기존 관리자 bearer session
|
||||
- 허용 역할: 기존 각 수정 endpoint의 관리자 권한
|
||||
- 거부 조건: 기존 401·403 및 공통 인증 만료 정책 유지
|
||||
- 리소스 소유권: path의 `characterId`와 각 resource ID 기준 서버 검증 유지
|
||||
- read-only 조건: 비활성 캐릭터와 모바일 mutation 제한 등 기존 기능별 capability 유지
|
||||
|
||||
## 6. 핵심 사용자 흐름
|
||||
|
||||
1. 관리자가 기존 상세·목록에서 수정 화면 또는 Sheet를 연다.
|
||||
2. 프론트엔드는 조회 응답을 수정 기준값으로 보존한다.
|
||||
3. 관리자가 하나 이상의 필드 또는 교체 파일을 변경한다.
|
||||
4. 프론트엔드는 기존 직렬화 규칙을 적용한 현재 값과 기준값을 필드별로 비교해 변경 필드만 request에 넣는다.
|
||||
5. 저장 성공·실패와 다음 화면 이동은 기존 기능 동작을 유지한다.
|
||||
|
||||
변경 필드가 없으면 저장 버튼은 비활성화되고 mutation 요청은 발생하지 않는다. 파일만 변경한 multipart 수정은 파일 파트와 빈 JSON object인 `request: {}`를 전송한다.
|
||||
|
||||
## 7. 정보 구조와 라우팅
|
||||
|
||||
```text
|
||||
/ai-characters/:characterId/edit
|
||||
/ai-characters/:characterId/audio-contents/:contentId/edit
|
||||
/ai-characters/:characterId/community-posts # 목록 내 게시글 Sheet
|
||||
/ai-characters/:characterId/series/:seriesId/edit
|
||||
/ai-characters/:characterId/fan-talks # 목록 내 답글 Sheet
|
||||
```
|
||||
|
||||
- route, path parameter, query parameter와 성공 후 이동 위치는 변경하지 않는다.
|
||||
- 커뮤니티 게시글과 FanTalk 답글은 별도 수정 route 없이 기존 Sheet에서 수정한다.
|
||||
- 직접 링크·새로고침·존재하지 않음·비활성 리소스 처리는 기존 정책을 유지한다.
|
||||
|
||||
## 8. 기능 요구사항
|
||||
|
||||
### 8.1 공통 변경 감지와 전송
|
||||
|
||||
| ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 |
|
||||
|---|---|---|---|---|
|
||||
| `DIFF-001` | 확정 | 수정 request JSON은 최종 변경 필드만 포함한다. | 한 필드 변경 시 해당 key만 존재하고 미변경 key는 0개다. | API Contract §2, `P1-T1`~`P1-T5` |
|
||||
| `DIFF-002` | 확정 | 변경 여부는 각 기능의 기존 trim·빈값→`null`·list 직렬화 규칙을 현재 값과 기준값에 동일하게 적용한 뒤 판정한다. | 공백 정리 후 원래 값과 같거나 변경 후 되돌린 필드는 생략된다. | API Contract §1.2, `P1-T1`~`P1-T5` |
|
||||
| `DIFF-003` | 확정 | 필드 삭제는 계약상 삭제 의미인 `null`을 보내고 미변경은 key를 생략한다. | nullable 값을 비우면 `{field:null}`, 건드리지 않으면 field key가 없다. | API Contract §1.3, `P1-T1`, `P1-T4` |
|
||||
| `DIFF-004` | 확정 | 변경 필드와 교체 파일이 모두 없으면 저장 버튼을 비활성화하고 mutation을 호출하지 않는다. | 최초 진입과 변경 후 원복 상태에서 저장 버튼 disabled, `PUT` 0건이다. | API Contract §1.4, `P1-T1`~`P1-T5` |
|
||||
| `DIFF-005` | 확정 | 파일만 바뀐 multipart 수정은 교체 파일과 빈 `request` JSON object를 보낸다. | 파일 파트 1개, request `{}`, 다른 JSON key 0개다. | API Contract §1.5, `P1-T1`~`P1-T4` |
|
||||
|
||||
### 8.2 기능별 수정 payload
|
||||
|
||||
| ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 |
|
||||
|---|---|---|---|---|
|
||||
| `CHAR-001` | 확정 | 캐릭터 수정은 변경된 프로필·선택·반복 필드와 새 profile image만 전송한다. | `name`만 변경하면 request는 `{name}`이고 `region`, `isActive`와 미변경 필드는 없다. | API Contract §2.1, `P1-T1` |
|
||||
| `AUDIO-001` | 확정 | 오디오 콘텐츠 수정은 변경된 `title`, `detail`, `tags`, `price`와 새 cover image만 전송한다. | `detail`만 변경하면 request는 `{detail}`이며 화면에 없는 boolean 필드는 없다. | API Contract §2.2, `P1-T2` |
|
||||
| `COMM-001` | 확정 | 커뮤니티 게시글 수정 저장은 변경된 `content`, `isCommentAvailable`, `isAdult`와 새 post image만 전송한다. | `content`만 변경하면 request는 `{content}`이고 `isFixed`는 없다. | API Contract §2.3, `P1-T3` |
|
||||
| `SERIES-001` | 확정 | 시리즈 수정은 변경된 기본·enum·nullable 필드와 새 image만 전송한다. | `title`만 변경하면 request는 `{title}`이고 이미 부분 적용된 `state` 포함 다른 미변경 필드는 없다. | API Contract §2.4, `P1-T4` |
|
||||
| `FANTALK-001` | 확정 | FanTalk 답글 수정은 기존 답글과 다른 `content`만 전송한다. | 변경 시 `{content}` 1개, 미변경 시 수정 버튼 disabled와 `PUT` 0건이다. | API Contract §2.5, `P1-T5` |
|
||||
|
||||
### 8.3 공통 파일·데이터 정책
|
||||
|
||||
| ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 |
|
||||
|---|---|---|---|---|
|
||||
| `FILE-001` | 확정 | 새 파일을 선택하지 않으면 기존 이미지·커버를 유지하고 파일 파트를 생략한다. | 미선택 수정 request에서 관련 파일 part가 없다. | API Contract §1.5, `P1-T1`~`P1-T4` |
|
||||
| `DATA-001` | 확정 | 숫자, boolean, enum, 배열과 객체 배열은 타입을 유지한 채 비교·전송한다. | `false`, `0`, 빈 값 삭제용 `null`이 누락되지 않고 배열 변경은 전체 해당 필드 값으로 전송된다. | API Contract §1.3, `P1-T1`~`P1-T4` |
|
||||
| `DATA-002` | 확정 | 원작 미선택·선택 해제 시 `originalWorkId` key를 생략하는 기존 계약을 유지한다. | 기존 `serializeCharacterRequest` 계약 test가 유지되고 원작 연결 해제 동작은 새로 만들지 않는다. | API Contract §2.1, `P1-T1` |
|
||||
|
||||
## 9. 반응형 기능 범위
|
||||
|
||||
| 기능 | Desktop | Tablet | Mobile | 비고 |
|
||||
|---|---:|---:|---:|---|
|
||||
| 캐릭터·오디오 콘텐츠·시리즈 수정 | 허용 | 허용 | 기존 조회 전용 | 기존 직접 route 차단 유지 |
|
||||
| 커뮤니티 게시글 수정 | 허용 | 허용 | 기존 capability 유지 | 기존 Sheet 정책 유지 |
|
||||
| FanTalk 답글 수정 | 허용 | 허용 | 허용 | 기존 Sheet 정책 유지 |
|
||||
|
||||
- 이번 변경으로 viewport breakpoint나 action 노출 정책을 바꾸지 않는다.
|
||||
- 기존 최소 viewport, 200% zoom, touch target과 virtual keyboard 검증을 회귀 Gate로 사용한다.
|
||||
|
||||
## 10. UI/UX Expectations
|
||||
|
||||
### 10.1 디자인과 component 원칙
|
||||
|
||||
- 기존 component와 design token을 그대로 사용한다.
|
||||
- 수정용 payload는 각 도메인의 기존 form serializer 또는 component에서 계산한다.
|
||||
- 새 dependency나 범용 diff abstraction을 만들지 않고 기능별 DTO 의미를 코드 가까이에 둔다.
|
||||
|
||||
### 10.2 화면 상태
|
||||
|
||||
- 최초 진입과 모든 변경을 원복한 상태에서는 저장 버튼을 disabled로 표시한다.
|
||||
- 파일 준비·저장 pending·오류·성공 상태와 중복 제출 방지는 기존 동작을 유지한다.
|
||||
- payload 생성 결과와 저장 버튼 활성화 조건은 같은 `hasChanges` 판정을 사용한다.
|
||||
|
||||
### 10.3 접근성
|
||||
|
||||
- disabled 상태는 native `disabled` 속성으로 노출한다.
|
||||
- 기존 visible label, 연결 오류, keyboard focus 순서와 성공·오류 live region을 유지한다.
|
||||
- 지원 viewport와 200% zoom에서 핵심 control이 가려지지 않고 axe critical·serious 위반 0건을 유지한다.
|
||||
|
||||
## 11. API 계약
|
||||
|
||||
### 11.1 공통 규칙
|
||||
|
||||
- base URL·인증 header·locale·성공 envelope·오류 envelope는 기존 [정식 OpenAPI](../20260725_AI캐릭터관리자웹/api-contract.openapi.json)를 유지한다.
|
||||
- HTTP method는 기존 `PUT`을 유지한다.
|
||||
- 캐릭터·오디오 콘텐츠·커뮤니티 게시글·시리즈는 `multipart/form-data`의 `request` JSON part를 유지한다.
|
||||
- FanTalk 답글은 `application/json`을 유지한다.
|
||||
- request field의 생략은 미변경, 명시적 `null`은 해당 DTO가 정의한 값 삭제를 의미한다.
|
||||
|
||||
### 11.2 Endpoint 추적
|
||||
|
||||
| 요구사항 | Method | Path | 계약 상태 | API Contract | 소유 Goal |
|
||||
|---|---|---|---|---|---|
|
||||
| `CHAR-001` | PUT | `/api/v2/admin/ai-characters/{characterId}` | 제공됨 | §2.1 | `P1-T1` |
|
||||
| `AUDIO-001` | PUT | `/api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}` | 제공됨 | §2.2 | `P1-T2` |
|
||||
| `COMM-001` | PUT | `/api/v2/admin/ai-characters/{characterId}/community-posts/{postId}` | 제공됨 | §2.3 | `P1-T3` |
|
||||
| `SERIES-001` | PUT | `/api/v2/admin/ai-characters/{characterId}/series/{seriesId}` | 제공됨 | §2.4 | `P1-T4` |
|
||||
| `FANTALK-001` | PUT | `/api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}/replies/{replyId}` | 제공됨 | §2.5 | `P1-T5` |
|
||||
|
||||
### 11.3 외부 제공 대기 계약
|
||||
|
||||
없음. 정식 OpenAPI에서 대상 update field가 모두 optional이고 현재 프론트엔드 schema도 부분 request를 허용한다.
|
||||
|
||||
## 12. 보안과 데이터 취급
|
||||
|
||||
- 인증 저장·만료 lifecycle과 401·403 처리는 기존 공통 API client 정책을 유지한다.
|
||||
- token, 파일 본문, signed URL과 관리자 입력 전문을 새 log·분석 이벤트에 기록하지 않는다.
|
||||
- 파일 확장자·MIME·크기·crop 검증과 resource ownership 검증을 변경하지 않는다.
|
||||
- 부분 request를 이유로 client가 권한 또는 서버 validation을 대신하지 않는다.
|
||||
- 감사 로그 추가는 이번 범위에 포함하지 않는다.
|
||||
|
||||
## 13. 성능과 품질 요구사항
|
||||
|
||||
- payload 크기는 같거나 작아야 하며 미변경 저장 network 요청은 0건이어야 한다.
|
||||
- 변경 판정은 현재 form field 수에 대한 동기 비교로 처리하고 새 network 조회나 dependency를 추가하지 않는다.
|
||||
- mutation single-flight, 파일 준비 취소·오류·재시도와 기존 browser 지원 범위를 유지한다.
|
||||
- test stack은 Vitest·Testing Library·Playwright mock E2E, TypeScript typecheck, ESLint, Vite production build를 사용한다.
|
||||
- backend 구현 전 mock fallback은 필요하지 않다. 기존 server/mock mode 경계를 유지하고 production 자동 mock fallback을 추가하지 않는다.
|
||||
|
||||
## 14. 성공 기준
|
||||
|
||||
### 14.1 기능 수용 기준
|
||||
|
||||
- [x] 다섯 수정 기능에서 한 필드 변경 request는 해당 key만 포함한다. (`DIFF-001`, `P1-GATE`)
|
||||
- [x] nullable 필드 삭제와 미변경 생략이 구분된다. (`DIFF-003`, `P1-T1`, `P1-T4`)
|
||||
- [x] 파일 미변경은 파일 part 생략, 파일만 변경은 파일 part와 `request: {}`를 전송한다. (`DIFF-005`, `FILE-001`)
|
||||
- [x] 최초 진입과 변경 후 원복 상태에서 저장 버튼이 disabled이고 mutation 요청이 없다. (`DIFF-004`)
|
||||
- [x] 기존 생성·비활성화·고정·순서·삭제 흐름이 회귀하지 않는다. (`P1-GATE`)
|
||||
|
||||
### 14.2 UI/UX 수용 기준
|
||||
|
||||
- [x] 기존 loading·error·success·pending 상태가 유지된다.
|
||||
- [x] keyboard-only로 기존 수정 흐름을 완료할 수 있다.
|
||||
- [x] 기존 지원 viewport와 200% zoom에서 핵심 control이 가려지지 않는다.
|
||||
- [x] axe critical·serious 위반이 0건이다.
|
||||
|
||||
### 14.3 추적성 완료 기준
|
||||
|
||||
- [x] 모든 확정 요구사항이 API Contract와 하나 이상의 Task·Goal 완료 증거로 연결된다.
|
||||
- [x] 각 구현 Task에 RED·GREEN·REFACTOR 결과가 Progress에 누적된다.
|
||||
- [x] Phase Gate의 자동·수동 payload 검증 결과가 기록된다.
|
||||
- [x] 미결·외부 의존·제외 상태의 새 항목이 생기면 담당·영향·재개 조건 또는 Decision Log가 추가된다.
|
||||
|
||||
## 15. Open Questions
|
||||
|
||||
열린 질문 없음.
|
||||
|
||||
인터뷰 결과:
|
||||
|
||||
- 최종 모호성: `0.07`
|
||||
- 명확성: Goal `1.00`, Scope `0.85`, Constraints `0.90`, Success `0.90`, Context `1.00`
|
||||
- 확정 결정: 변경 필드만 전송하며, 변경이 없으면 저장 버튼 비활성화와 mutation 요청 0건
|
||||
|
||||
## 16. 요구사항 추적표
|
||||
|
||||
| 요구사항 범위 | API Contract | 계획 Phase | Goal | 자동 검증 | 수동 검증 |
|
||||
|---|---|---:|---|---|---|
|
||||
| `DIFF-001~005`, `FILE-001`, `DATA-001` | §1 | 1 | `P1-T1`~`P1-T5`, `P1-GATE` | 기능별 form·contract test | DevTools Network payload·무요청 확인 |
|
||||
| `CHAR-001`, `DATA-002` | §2.1 | 1 | `P1-T1` | Character edit/API test | 캐릭터 단일 필드·파일 수정 |
|
||||
| `AUDIO-001` | §2.2 | 1 | `P1-T2` | Audio form/API test | 오디오 단일 필드·cover 수정 |
|
||||
| `COMM-001` | §2.3 | 1 | `P1-T3` | Community Sheet/API test | 게시글 단일 필드·image 수정 |
|
||||
| `SERIES-001` | §2.4 | 1 | `P1-T4` | Series form/API test | 시리즈 단일 필드·image 수정 |
|
||||
| `FANTALK-001` | §2.5 | 1 | `P1-T5` | FanTalk reply/API test | 답글 변경·무변경 수정 |
|
||||
|
||||
## 17. Decision Log
|
||||
|
||||
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 요구사항·계약·Goal |
|
||||
|---|---|---|---|---|---|
|
||||
| 2026-08-06 | `DEC-001` | 확정 | 수정 request는 HTTP method를 바꾸지 않고 최종 변경 필드만 포함한다. | 사용자 요청과 정식 OpenAPI의 optional update field | `DIFF-001~003`, API Contract §1, `P1-T1`~`P1-T5` |
|
||||
| 2026-08-06 | `DEC-002` | 확정 | 변경 필드와 교체 파일이 없으면 저장 버튼을 비활성화하고 요청하지 않는다. | 사용자 인터뷰 A안 선택 | `DIFF-004`, `P1-T1`~`P1-T5` |
|
||||
| 2026-08-06 | `DEC-003` | 확정 | 파일만 변경한 multipart update는 required `request` part를 빈 object로 전송한다. | 기존 multipart 계약에서 `request` part가 required이고 update object에는 required field가 없음 | `DIFF-005`, API Contract §1.5, `P1-T1`~`P1-T4` |
|
||||
| 2026-08-06 | `DEC-004` | 확정 | 생성·비활성화·고정·순서·삭제 전용 mutation은 범위에서 제외한다. | 해당 action은 이미 전용 최소 payload 또는 별도 method를 사용함 | Non-Goals, `P1-GATE` |
|
||||
| 2026-08-06 | `DEC-005` | 확정 | 새 공통 diff abstraction이나 dependency 없이 각 도메인의 기존 serializer와 원본 DTO 비교를 사용한다. | DTO별 `null`, 배열, 파일과 수정 가능 필드 의미가 다름 | §10.1, `P1-T1`~`P1-T5` |
|
||||
|
||||
## 18. 변경 관리
|
||||
|
||||
- [x] Decision Log에 변경 이유와 날짜를 기록한다.
|
||||
- [x] 관련 요구사항 상태·본문·수용 기준을 갱신한다.
|
||||
- [x] API Contract의 request 규칙과 예시를 갱신한다.
|
||||
- [x] `plan-task.md`의 범위·Files·Interfaces·체크박스·완료 증거를 코드 변경 전에 갱신한다.
|
||||
- [x] 기존 Progress·review·검증 기록을 삭제하거나 덮어쓰지 않는다.
|
||||
Reference in New Issue
Block a user