From 7a2c70a260fe8a8b570b81620138a413e1403fdd Mon Sep 17 00:00:00 2001 From: ttipo Date: Fri, 19 Jun 2026 17:03:59 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=A0=80=EC=9E=A5=EC=86=8C=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EC=9D=B4=EB=A6=84=20=EC=8B=A4=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=EA=B0=80=EC=9A=A9=EC=84=B1=20=EC=B2=B4=ED=81=AC=20+=20?= =?UTF-8?q?=EC=86=8C=EC=9C=A0=EC=9E=90=20=EC=95=84=EB=B0=94=ED=83=80=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GET /repos/name-available?slug= 추가(:repoId 앞 선언, Relay slug 중복만 검사) - 생성 폼: 형식 즉시 검사 + 350ms 디바운스 서버 조회로 상태별(빈칸/형식위반/확인중/사용가능/중복) 색·아이콘 표기 - 항상 떠 있던 정적 "사용 가능한 이름입니다" 표시 제거 - 조직 이니셜 소유자 아바타 제거 Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/src/modules/repo/repo.controller.ts | 8 ++ backend/src/modules/repo/repo.service.ts | 11 +++ frontend/src/composables/useRepo.ts | 10 +- frontend/src/pages/relay/RepoCreatePage.vue | 103 ++++++++++++++++---- 4 files changed, 114 insertions(+), 18 deletions(-) diff --git a/backend/src/modules/repo/repo.controller.ts b/backend/src/modules/repo/repo.controller.ts index 5d81a4d..fc088d8 100644 --- a/backend/src/modules/repo/repo.controller.ts +++ b/backend/src/modules/repo/repo.controller.ts @@ -65,6 +65,14 @@ export class RepoController { return this.repoService.getCreateMeta(); } + // ':repoId' 보다 먼저 선언 — 생성 폼의 이름(slug) 실시간 가용성 체크 + @Get('name-available') + @ApiOperation({ summary: '저장소 이름(slug) 사용 가능 여부' }) + @ApiResponse({ status: 200, description: '확인 성공' }) + checkName(@Query('slug') slug: string): Promise<{ available: boolean }> { + return this.repoService.isNameAvailable(slug ?? ''); + } + @Get(':repoId') @ApiOperation({ summary: '저장소 단건 조회' }) @ApiResponse({ status: 200, description: '조회 성공' }) diff --git a/backend/src/modules/repo/repo.service.ts b/backend/src/modules/repo/repo.service.ts index a255430..5edae9d 100644 --- a/backend/src/modules/repo/repo.service.ts +++ b/backend/src/modules/repo/repo.service.ts @@ -116,6 +116,17 @@ export class RepoService { }); } + // 저장소 이름(slug) 사용 가능 여부 — 생성 폼의 실시간 체크용(Relay 중복만 검사). + // 빈 값/형식은 프론트가 거르고, Gitea 이름 충돌은 생성 시 최종 확인한다. + async isNameAvailable(slug: string): Promise<{ available: boolean }> { + const trimmed = (slug ?? '').trim(); + if (!trimmed) return { available: false }; + const existing = await this.repoRepo.findOne({ + where: { slugName: trimmed }, + }); + return { available: !existing }; + } + // 저장소 단건 — 조직 모델상 인증 사용자면 열람 가능(쓰기 권한은 별도 가드). // 응답에 현재 사용자의 권한(canManage/isOwner)을 함께 내려, 프론트가 설정 탭 노출·진입을 결정한다. async findOne(slugName: string, userId: string): Promise { diff --git a/frontend/src/composables/useRepo.ts b/frontend/src/composables/useRepo.ts index ef76073..0d866d0 100644 --- a/frontend/src/composables/useRepo.ts +++ b/frontend/src/composables/useRepo.ts @@ -22,6 +22,14 @@ export function useRepo() { return (await api.get('/repos/meta')) as unknown as RepoCreateMeta } + // 이름(slug) 사용 가능 여부 — 생성 폼 실시간 체크 (실패해도 전역 알림 X) + async function checkName(slug: string): Promise<{ available: boolean }> { + return (await api.get('/repos/name-available', { + params: { slug }, + silent: true, + })) as unknown as { available: boolean } + } + async function get(id: string): Promise { return (await api.get(`/repos/${id}`)) as unknown as ApiRepo } @@ -38,5 +46,5 @@ export function useRepo() { await api.delete(`/repos/${id}`) } - return { list, meta, get, create, update, remove } + return { list, meta, checkName, get, create, update, remove } } diff --git a/frontend/src/pages/relay/RepoCreatePage.vue b/frontend/src/pages/relay/RepoCreatePage.vue index 56aedc3..3113011 100644 --- a/frontend/src/pages/relay/RepoCreatePage.vue +++ b/frontend/src/pages/relay/RepoCreatePage.vue @@ -1,18 +1,19 @@