fix: 화상회의 후속 보정(권한 헤더·빈 상태·한글 방이름 등)

- 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) <noreply@anthropic.com>
This commit is contained in:
2026-06-24 18:17:22 +09:00
parent 30b4b87368
commit d8d2ff21f9
9 changed files with 26 additions and 137 deletions
@@ -126,6 +126,8 @@ function select(value: T): void {
}
.dd.block .dd-label {
flex: 1;
/* button 기본 text-align:center 상속으로 가운데 보이는 것을 폼 셀렉트답게 좌측 정렬 */
text-align: left;
}
/* 트리거 — 기존 정렬/필터 pill 과 동일한 외형 */
@@ -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)) {
@@ -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 {
<span class="count-pill">{{ rooms.length }}</span>
</div>
<p class="lede">
회의 방을 만들거나 목록에서 선택해 입장하세요. 공유 링크(<code>/meeting/&lt;방이름&gt;</code>)로도 바로 입장할 있습니다.
회의 방을 만들거나 목록에서 선택해 입장하세요.
</p>
<!-- 툴바: 검색 + 회의 만들기 -->
@@ -158,22 +159,12 @@ function fmtSchedule(iso: string): string {
</button>
</div>
</div>
<div
v-if="rooms.length === 0"
class="rl-empty"
>
<MeetingIcon name="cam" />
<p>아직 생성된 회의방이 없습니다.</p>
<span>"새 회의 만들기" 회의방을 만들어 보세요.</span>
</div>
<div
v-else-if="filteredRooms.length === 0"
class="rl-empty"
>
<MeetingIcon name="search" />
<p>검색 결과가 없습니다.</p>
<span>다른 검색어로 다시 시도해 보세요.</span>
</div>
<EmptyState v-if="rooms.length === 0">
아직 생성된 회의방이 없습니다. 회의를 만들어 보세요.
</EmptyState>
<EmptyState v-else-if="filteredRooms.length === 0">
검색 결과가 없습니다.
</EmptyState>
</div>
</div>
</template>
@@ -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 {
@@ -115,12 +115,6 @@ function enter() {
<MeetingIcon :name="camOn ? 'cam' : 'camOff'" />
</button>
</div>
<div
v-if="denied"
class="pj-camoff-note"
>
카메라·마이크 권한이 없습니다
</div>
</div>
<div class="pj-side">
@@ -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;
@@ -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(
</div>
</div>
<p
v-if="errorMsg"
class="room-err"
>
<span class="re-ic"><MeetingIcon name="warn" /></span>
<span>{{ errorMsg }}</span>
<button
type="button"
class="re-x"
@click="emit('dismiss-error')"
>
<MeetingIcon name="close" />
</button>
</p>
<!-- 본문 -->
<div class="room-body">
<div class="room-main">
@@ -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;
+2 -5
View File
@@ -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"
/>
</div>
</AppShell>