From d8d2ff21f9f36519e658079b7991aa7b0701b2a3 Mon Sep 17 00:00:00 2001 From: ttipo Date: Wed, 24 Jun 2026 18:17:22 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=ED=99=94=EC=83=81=ED=9A=8C=EC=9D=98=20?= =?UTF-8?q?=ED=9B=84=EC=86=8D=20=EB=B3=B4=EC=A0=95(=EA=B6=8C=ED=95=9C=20?= =?UTF-8?q?=ED=97=A4=EB=8D=94=C2=B7=EB=B9=88=20=EC=83=81=ED=83=9C=C2=B7?= =?UTF-8?q?=ED=95=9C=EA=B8=80=20=EB=B0=A9=EC=9D=B4=EB=A6=84=20=EB=93=B1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - nginx Permissions-Policy: 운영에서 camera/microphone 을 self 에 허용 (기존 microphone=()/camera=() 가 LiveKit 장치 접근을 차단하던 문제 수정) - 회의 로비 빈 상태/검색결과 없음을 공용 EmptyState 로 통일(내 업무·프로젝트 화면과 동일) - 미구현인 공유 링크 안내 문구 제거(lede) - 프리조인 "카메라·마이크 권한이 없습니다" 문구 제거 - 회의실 상단 노란색 권한 안내 배너(room-err) 제거 + 관련 prop/이벤트 정리 - 회의방 이름에 한글 허용(백엔드 DTO 2종 + 프론트 검증 정규식/문구, 공백·특수문자는 계속 차단) - 멤버 초대 등급 셀렉트(DropdownSelect block) 라벨 좌측 정렬(버튼 기본 가운데정렬 상속 수정) Co-Authored-By: Claude Opus 4.8 (1M context) --- .../meeting/dto/create-meeting-room.dto.ts | 9 +-- .../meeting/dto/create-meeting-token.dto.ts | 9 +-- frontend/nginx.conf | 3 +- frontend/src/components/DropdownSelect.vue | 2 + .../components/meeting/MeetingCreateForm.vue | 4 +- .../src/components/meeting/MeetingLobby.vue | 57 +++---------------- .../src/components/meeting/MeetingPrejoin.vue | 17 ------ .../src/components/meeting/MeetingRoom.vue | 55 ------------------ frontend/src/pages/relay/MeetingPage.vue | 7 +-- 9 files changed, 26 insertions(+), 137 deletions(-) diff --git a/backend/src/modules/meeting/dto/create-meeting-room.dto.ts b/backend/src/modules/meeting/dto/create-meeting-room.dto.ts index 991d66c..5eccba3 100644 --- a/backend/src/modules/meeting/dto/create-meeting-room.dto.ts +++ b/backend/src/modules/meeting/dto/create-meeting-room.dto.ts @@ -13,13 +13,14 @@ import { // 화상회의 방 생성 요청 export class CreateMeetingRoomDto { @ApiProperty({ - description: '방 이름(영문/숫자/-/_ 만 허용, 고유)', - example: 'weekly-sync', + description: '방 이름(한글/영문/숫자/-/_ 허용, 공백 불가, 고유)', + example: '주간-회의', }) @IsString() @Length(1, 64, { message: '방 이름은 1~64자여야 합니다.' }) - @Matches(/^[A-Za-z0-9_-]+$/, { - message: '방 이름은 영문, 숫자, 하이픈(-), 밑줄(_)만 사용할 수 있습니다.', + @Matches(/^[A-Za-z0-9가-힣ㄱ-ㅎㅏ-ㅣ_-]+$/, { + message: + '방 이름은 한글, 영문, 숫자, 하이픈(-), 밑줄(_)만 사용할 수 있습니다. (공백 불가)', }) name!: string; diff --git a/backend/src/modules/meeting/dto/create-meeting-token.dto.ts b/backend/src/modules/meeting/dto/create-meeting-token.dto.ts index d6269b8..bf4cc5a 100644 --- a/backend/src/modules/meeting/dto/create-meeting-token.dto.ts +++ b/backend/src/modules/meeting/dto/create-meeting-token.dto.ts @@ -4,13 +4,14 @@ import { IsString, Length, Matches } from 'class-validator'; // 화상회의 입장 토큰 발급 요청 — 참여할 방 이름을 전달한다. export class CreateMeetingTokenDto { @ApiProperty({ - description: '참여할 회의 방 이름(영문/숫자/-/_ 만 허용)', - example: 'team-standup', + description: '참여할 회의 방 이름(한글/영문/숫자/-/_ 허용, 공백 불가)', + example: '주간-회의', }) @IsString() @Length(1, 64, { message: '방 이름은 1~64자여야 합니다.' }) - @Matches(/^[A-Za-z0-9_-]+$/, { - message: '방 이름은 영문, 숫자, 하이픈(-), 밑줄(_)만 사용할 수 있습니다.', + @Matches(/^[A-Za-z0-9가-힣ㄱ-ㅎㅏ-ㅣ_-]+$/, { + message: + '방 이름은 한글, 영문, 숫자, 하이픈(-), 밑줄(_)만 사용할 수 있습니다. (공백 불가)', }) roomName!: string; } diff --git a/frontend/nginx.conf b/frontend/nginx.conf index 7309278..d443297 100644 --- a/frontend/nginx.conf +++ b/frontend/nginx.conf @@ -9,7 +9,8 @@ server { add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; - add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always; + # 화상회의(LiveKit)용 카메라/마이크는 자기 출처(self)에 허용. 위치정보는 차단 유지. + add_header Permissions-Policy "geolocation=(), microphone=(self), camera=(self)" always; # ---------------------------------------- # 압축 (Gzip) diff --git a/frontend/src/components/DropdownSelect.vue b/frontend/src/components/DropdownSelect.vue index ad8f6a5..6501a46 100644 --- a/frontend/src/components/DropdownSelect.vue +++ b/frontend/src/components/DropdownSelect.vue @@ -126,6 +126,8 @@ function select(value: T): void { } .dd.block .dd-label { flex: 1; + /* button 기본 text-align:center 상속으로 가운데 보이는 것을 폼 셀렉트답게 좌측 정렬 */ + text-align: left; } /* 트리거 — 기존 정렬/필터 pill 과 동일한 외형 */ diff --git a/frontend/src/components/meeting/MeetingCreateForm.vue b/frontend/src/components/meeting/MeetingCreateForm.vue index 64eab65..ca4d7c8 100644 --- a/frontend/src/components/meeting/MeetingCreateForm.vue +++ b/frontend/src/components/meeting/MeetingCreateForm.vue @@ -22,7 +22,7 @@ const emit = defineEmits<{ (e: 'cancel'): void }>() -const NAME_RE = /^[A-Za-z0-9_-]{1,64}$/ +const NAME_RE = /^[A-Za-z0-9가-힣ㄱ-ㅎㅏ-ㅣ_-]{1,64}$/ const userApi = useUser() const meetingApi = useMeeting() @@ -72,7 +72,7 @@ async function submit() { if (submitting.value) return const n = name.value.trim() if (!NAME_RE.test(n)) { - error.value = '방 이름은 영문·숫자·-·_ 1~64자만 가능합니다.' + error.value = '방 이름은 한글·영문·숫자·-·_ 1~64자만 가능합니다.' return } if (props.rooms.some((r) => r.name === n)) { diff --git a/frontend/src/components/meeting/MeetingLobby.vue b/frontend/src/components/meeting/MeetingLobby.vue index 6510976..e227787 100644 --- a/frontend/src/components/meeting/MeetingLobby.vue +++ b/frontend/src/components/meeting/MeetingLobby.vue @@ -3,6 +3,7 @@ import { computed, ref } from 'vue' import MeetingIcon from './MeetingIcon.vue' import MeetingCreateForm from './MeetingCreateForm.vue' +import EmptyState from '@/components/common/EmptyState.vue' import { relativeTimeKo, formatDate } from '@/shared/utils/format' import type { ApiMeetingRoom } from '@/types/meeting' @@ -59,7 +60,7 @@ function fmtSchedule(iso: string): string { {{ rooms.length }}

- 회의 방을 만들거나 목록에서 선택해 입장하세요. 공유 링크(/meeting/<방이름>)로도 바로 입장할 수 있습니다. + 회의 방을 만들거나 목록에서 선택해 입장하세요.

@@ -158,22 +159,12 @@ function fmtSchedule(iso: string): string { -
- -

아직 생성된 회의방이 없습니다.

- "새 회의 만들기"로 첫 회의방을 만들어 보세요. -
-
- -

검색 결과가 없습니다.

- 다른 검색어로 다시 시도해 보세요. -
+ + 아직 생성된 회의방이 없습니다. 새 회의를 만들어 보세요. + + + 검색 결과가 없습니다. + @@ -201,14 +192,6 @@ function fmtSchedule(iso: string): string { font-size: 0.844rem; margin: 0.25rem 0 1.125rem; } -code { - font-family: ui-monospace, SFMono-Regular, Menlo, monospace; - font-size: 0.92em; - background: #eceef1; - padding: 1px 0.3125rem; - border-radius: 0.25rem; - color: var(--text-2); -} /* 툴바: 검색(.toolbar/.search 는 전역) + 우측 새 회의 버튼 */ .new-room { @@ -344,30 +327,6 @@ code { width: 0.9375rem; height: 0.9375rem; } -.rl-empty { - display: flex; - flex-direction: column; - align-items: center; - gap: 0.375rem; - padding: 3.125rem 1.25rem; - background: var(--panel); - border: 1px dashed var(--border-strong); - border-radius: 0.75rem; - color: var(--text-3); -} -.rl-empty svg { - width: 2rem; - height: 2rem; - color: var(--border-strong); -} -.rl-empty p { - font-size: 0.875rem; - font-weight: 600; - color: var(--text-2); -} -.rl-empty span { - font-size: 0.78125rem; -} /* 버튼 기본/primary 는 전역 .btn 사용. 여기선 danger·비활성·사이즈 변형만 보강. */ .btn.danger { diff --git a/frontend/src/components/meeting/MeetingPrejoin.vue b/frontend/src/components/meeting/MeetingPrejoin.vue index 5ac363b..0eeb1b9 100644 --- a/frontend/src/components/meeting/MeetingPrejoin.vue +++ b/frontend/src/components/meeting/MeetingPrejoin.vue @@ -115,12 +115,6 @@ function enter() { -
- 카메라·마이크 권한이 없습니다 -
@@ -272,17 +266,6 @@ function enter() { opacity: 0.5; cursor: not-allowed; } -.pj-camoff-note { - position: absolute; - left: 0; - right: 0; - top: 42%; - transform: translateY(-50%); - text-align: center; - color: var(--d-text-3); - font-size: 0.8125rem; - z-index: 1; -} .pj-side { width: 17.875rem; flex-shrink: 0; diff --git a/frontend/src/components/meeting/MeetingRoom.vue b/frontend/src/components/meeting/MeetingRoom.vue index 587f214..fce3cb6 100644 --- a/frontend/src/components/meeting/MeetingRoom.vue +++ b/frontend/src/components/meeting/MeetingRoom.vue @@ -18,7 +18,6 @@ interface Props { micEnabled: boolean camEnabled: boolean isHost: boolean - errorMsg: string | null messages: ChatMessage[] // 말하는 사람 하이라이트 사용 여부(기본 on) highlight?: boolean @@ -30,7 +29,6 @@ const emit = defineEmits<{ (e: 'promote', identity: string): void (e: 'send', text: string): void (e: 'leave'): void - (e: 'dismiss-error'): void }>() // 한 페이지에 보일 참여자 수 — 스피커 뷰는 진행자(스테이지) 제외 나머지 최대 50명, @@ -158,21 +156,6 @@ watch(
-

- - {{ errorMsg }} - -

-
@@ -347,44 +330,6 @@ watch( width: 0.9375rem; height: 0.9375rem; } -.room-err { - display: flex; - align-items: center; - gap: 0.625rem; - margin: 0.75rem 1.125rem 0; - padding: 0.6875rem 0.875rem; - background: rgba(183, 121, 31, 0.16); - border: 1px solid rgba(183, 121, 31, 0.45); - border-radius: 0.5625rem; - color: var(--d-amber); - font-size: 0.8125rem; -} -.room-err .re-ic { - display: grid; - place-items: center; -} -.room-err .re-ic svg { - width: 1.0625rem; - height: 1.0625rem; -} -.room-err span { - flex: 1; -} -.re-x { - border: none; - background: transparent; - color: var(--d-amber); - cursor: pointer; - display: grid; - place-items: center; - padding: 0.125rem; - flex: 0 0 auto; -} -.re-x svg { - width: 0.9375rem; - height: 0.9375rem; -} - .room-body { flex: 1; display: flex; diff --git a/frontend/src/pages/relay/MeetingPage.vue b/frontend/src/pages/relay/MeetingPage.vue index ca70b61..6d6f806 100644 --- a/frontend/src/pages/relay/MeetingPage.vue +++ b/frontend/src/pages/relay/MeetingPage.vue @@ -25,7 +25,6 @@ const { connecting, micEnabled, camEnabled, - errorMsg, featuredIdentity, messages, connect, @@ -36,7 +35,7 @@ const { sendChat, } = useLiveKitRoom() -const ROOM_PATTERN = /^[A-Za-z0-9_-]{1,64}$/ +const ROOM_PATTERN = /^[A-Za-z0-9가-힣ㄱ-ㅎㅏ-ㅣ_-]{1,64}$/ // 프리조인 대상 방 정보(표시용) interface PrejoinTarget { @@ -84,7 +83,7 @@ function onJoin(name: string) { } if (!ROOM_PATTERN.test(n)) { joinError.value = - '방 이름은 영문·숫자·-·_ 1~64자만 가능합니다. (한글·공백 불가)' + '방 이름은 한글·영문·숫자·-·_ 1~64자만 가능합니다. (공백 불가)' return } const room = rooms.value.find((r) => r.name === n) @@ -228,14 +227,12 @@ onBeforeUnmount(() => { :mic-enabled="micEnabled" :cam-enabled="camEnabled" :is-host="isHost" - :error-msg="errorMsg" :messages="messages" @toggle-mic="toggleMic" @toggle-cam="toggleCam" @promote="promote" @send="sendChat" @leave="onLeave" - @dismiss-error="errorMsg = null" />