Compare commits
2 Commits
30b4b87368
...
960ff1d420
| Author | SHA1 | Date | |
|---|---|---|---|
| 960ff1d420 | |||
| d8d2ff21f9 |
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+2
-1
@@ -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)
|
||||
|
||||
@@ -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/<방이름></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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user