refactor: 공용 폼 모달 BaseModal + 전역 .form-* 필드 클래스 (Phase 1a)
- components/common/BaseModal.vue 신설: backdrop+헤더(제목/닫기)+본문/푸터 슬롯, Teleport·ESC·바깥클릭 닫기 내장. 전역 .modal* 와 충돌 없게 bm- 접두 클래스 사용 - relay.css 에 .form-field/-label/-row/-input/-textarea/-select(+wrap/chev) 표준 추가 - 일정·카테고리 모달을 BaseModal + .form-* 로 전환(중복 모달/필드 CSS 제거, 푸터 버튼은 전역 .mbtn 사용) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -569,6 +569,90 @@ body {
|
||||
color: var(--text-3);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* 폼 필드 (폼 모달 공통 — BaseModal 본문 등에서 사용)
|
||||
* ============================================================ */
|
||||
.form-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.form-label {
|
||||
font-size: 0.781rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-2);
|
||||
}
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.form-row .form-field {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.form-input {
|
||||
width: 100%;
|
||||
height: 2.5rem;
|
||||
padding: 0 0.75rem;
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: var(--radius);
|
||||
font-family: inherit;
|
||||
font-size: 0.844rem;
|
||||
color: var(--text);
|
||||
background: #fff;
|
||||
}
|
||||
.form-input::placeholder {
|
||||
color: var(--text-3);
|
||||
}
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px var(--accent-weak);
|
||||
}
|
||||
.form-textarea {
|
||||
height: auto;
|
||||
min-height: 5.75rem;
|
||||
padding: 0.625rem 0.75rem;
|
||||
line-height: 1.5;
|
||||
resize: vertical;
|
||||
}
|
||||
/* 네이티브 select 래퍼 + 셰브론 */
|
||||
.form-select-wrap {
|
||||
position: relative;
|
||||
}
|
||||
.form-select {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
height: 2.5rem;
|
||||
padding: 0 2.125rem 0 0.75rem;
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: var(--radius);
|
||||
font-family: inherit;
|
||||
font-size: 0.844rem;
|
||||
color: var(--text);
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
.form-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px var(--accent-weak);
|
||||
}
|
||||
.form-select-chev {
|
||||
position: absolute;
|
||||
right: 0.625rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: grid;
|
||||
color: var(--text-3);
|
||||
pointer-events: none;
|
||||
}
|
||||
.form-select-chev svg {
|
||||
width: 0.9375rem;
|
||||
height: 0.9375rem;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* 더보기 버튼 (목록 페이지네이션 공통)
|
||||
* ============================================================ */
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
<script setup lang="ts">
|
||||
// 공용 폼 모달 셸 — backdrop + 헤더(제목/닫기) + 본문 슬롯 + 푸터 슬롯.
|
||||
// Teleport(body) · ESC · 바깥(backdrop) 클릭 닫기 내장.
|
||||
// 전역 .modal*(확인 다이얼로그 전용)과 충돌하지 않도록 bm- 접두 클래스를 쓴다.
|
||||
// 폼 필드는 전역 .form-* 클래스(relay.css)를 본문 슬롯에서 사용한다.
|
||||
import { onBeforeUnmount, onMounted } from 'vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
title: string
|
||||
// 너비: md(기본 33.75rem) | sm(27.5rem)
|
||||
size?: 'md' | 'sm'
|
||||
}>(),
|
||||
{ size: 'md' },
|
||||
)
|
||||
const emit = defineEmits<{ (e: 'close'): void }>()
|
||||
|
||||
function onKey(e: KeyboardEvent): void {
|
||||
if (e.key === 'Escape') emit('close')
|
||||
}
|
||||
onMounted(() => document.addEventListener('keydown', onKey))
|
||||
onBeforeUnmount(() => document.removeEventListener('keydown', onKey))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div
|
||||
class="bm-back"
|
||||
@mousedown.self="emit('close')"
|
||||
>
|
||||
<div
|
||||
class="bm"
|
||||
:class="{ 'bm-sm': size === 'sm' }"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<div class="bm-head">
|
||||
<h3>{{ title }}</h3>
|
||||
<button
|
||||
type="button"
|
||||
class="bm-x"
|
||||
aria-label="닫기"
|
||||
@click="emit('close')"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.2"
|
||||
stroke-linecap="round"
|
||||
><path d="M18 6 6 18M6 6l12 12" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="bm-body">
|
||||
<slot />
|
||||
</div>
|
||||
<div
|
||||
v-if="$slots.foot"
|
||||
class="bm-foot"
|
||||
>
|
||||
<slot name="foot" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.bm-back {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 100;
|
||||
background: rgba(20, 24, 33, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
.bm {
|
||||
width: 100%;
|
||||
max-width: 33.75rem;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
background: #fff;
|
||||
border-radius: 0.875rem;
|
||||
box-shadow: 0 24px 64px rgba(20, 24, 33, 0.32);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.bm.bm-sm {
|
||||
max-width: 27.5rem;
|
||||
}
|
||||
.bm-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1.375rem 1.5rem 1rem;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: #fff;
|
||||
border-radius: 0.875rem 0.875rem 0 0;
|
||||
z-index: 1;
|
||||
}
|
||||
.bm-head h3 {
|
||||
font-size: 1.0625rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.01875rem;
|
||||
}
|
||||
.bm-x {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-3);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
margin-right: -0.3125rem;
|
||||
}
|
||||
.bm-x:hover {
|
||||
background: #f1f2f4;
|
||||
color: var(--text);
|
||||
}
|
||||
.bm-x svg {
|
||||
width: 1.125rem;
|
||||
height: 1.125rem;
|
||||
}
|
||||
.bm-body {
|
||||
padding: 0.5rem 1.5rem 0.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.125rem;
|
||||
text-align: left;
|
||||
}
|
||||
.bm-foot {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5625rem;
|
||||
padding: 1rem 1.5rem;
|
||||
border-top: 1px solid var(--border);
|
||||
background: #fafbfc;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
border-radius: 0 0 0.875rem 0.875rem;
|
||||
}
|
||||
/* 푸터 버튼 — 전역 .mbtn(확인 다이얼로그용 flex:1) 대신 폼 모달용 우측정렬 컴팩트 */
|
||||
.bm-foot :deep(.mbtn) {
|
||||
flex: 0 0 auto;
|
||||
height: 2.375rem;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
.bm-foot :deep(.mbtn:disabled) {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.bm-foot :deep(.mbtn.primary:disabled),
|
||||
.bm-foot :deep(.mbtn.primary:disabled:hover) {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
// 카테고리 생성/수정 모달 — 이름 + 색상(hue 슬라이더)
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import BaseModal from '@/components/common/BaseModal.vue'
|
||||
import type {
|
||||
ApiScheduleCategory,
|
||||
ScheduleCategoryPayload,
|
||||
@@ -57,181 +58,60 @@ function submit(): void {
|
||||
if (!valid.value) return
|
||||
emit('save', { label: name.value.trim(), color: color.value })
|
||||
}
|
||||
function onKey(ev: KeyboardEvent): void {
|
||||
if (ev.key === 'Escape') emit('close')
|
||||
}
|
||||
onMounted(() => document.addEventListener('keydown', onKey))
|
||||
onBeforeUnmount(() => document.removeEventListener('keydown', onKey))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="modal-back"
|
||||
@mousedown.self="emit('close')"
|
||||
<BaseModal
|
||||
:title="isEdit ? '카테고리 편집' : '새 카테고리'"
|
||||
size="sm"
|
||||
@close="emit('close')"
|
||||
>
|
||||
<div class="modal modal-sm">
|
||||
<div class="modal-head">
|
||||
<h3>{{ isEdit ? '카테고리 편집' : '새 카테고리' }}</h3>
|
||||
<button
|
||||
class="modal-x"
|
||||
@click="emit('close')"
|
||||
<label class="form-field"><span class="form-label">이름</span>
|
||||
<input
|
||||
v-model="name"
|
||||
class="form-input"
|
||||
placeholder="카테고리 이름"
|
||||
autofocus
|
||||
@keydown.enter="submit"
|
||||
>
|
||||
</label>
|
||||
<div class="form-field">
|
||||
<span class="form-label">색상</span>
|
||||
<div class="cat-color">
|
||||
<span
|
||||
class="cat-dot"
|
||||
:style="{ background: color }"
|
||||
/>
|
||||
<input
|
||||
v-model.number="hue"
|
||||
type="range"
|
||||
min="0"
|
||||
max="359"
|
||||
class="hue-slider"
|
||||
:style="{ '--cur': color }"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.2"
|
||||
stroke-linecap="round"
|
||||
><path d="M18 6 6 18M6 6l12 12" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<label class="f-field"><span class="f-label">이름</span>
|
||||
<input
|
||||
v-model="name"
|
||||
class="f-input"
|
||||
placeholder="카테고리 이름"
|
||||
autofocus
|
||||
@keydown.enter="submit"
|
||||
>
|
||||
</label>
|
||||
<div class="f-field">
|
||||
<span class="f-label">색상</span>
|
||||
<div class="cat-color">
|
||||
<span
|
||||
class="cat-dot"
|
||||
:style="{ background: color }"
|
||||
/>
|
||||
<input
|
||||
v-model.number="hue"
|
||||
type="range"
|
||||
min="0"
|
||||
max="359"
|
||||
class="hue-slider"
|
||||
:style="{ '--cur': color }"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-foot">
|
||||
<button
|
||||
class="mbtn"
|
||||
@click="emit('close')"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
<button
|
||||
class="mbtn primary"
|
||||
:disabled="!valid"
|
||||
@click="submit"
|
||||
>
|
||||
{{ isEdit ? '저장' : '추가' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #foot>
|
||||
<button
|
||||
class="mbtn"
|
||||
@click="emit('close')"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
<button
|
||||
class="mbtn primary"
|
||||
:disabled="!valid"
|
||||
@click="submit"
|
||||
>
|
||||
{{ isEdit ? '저장' : '추가' }}
|
||||
</button>
|
||||
</template>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.modal-back {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 100;
|
||||
background: rgba(20, 24, 33, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
.modal {
|
||||
width: 100%;
|
||||
max-width: 33.75rem;
|
||||
background: #fff;
|
||||
border-radius: 0.875rem;
|
||||
box-shadow: 0 24px 64px rgba(20, 24, 33, 0.32);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.modal.modal-sm {
|
||||
max-width: 27.5rem;
|
||||
}
|
||||
.modal-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1.375rem 1.5rem 1rem;
|
||||
}
|
||||
.modal-head h3 {
|
||||
font-size: 1.0625rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.01875rem;
|
||||
}
|
||||
.modal-x {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-3);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
margin-right: -0.3125rem;
|
||||
}
|
||||
.modal-x:hover {
|
||||
background: #f1f2f4;
|
||||
color: var(--text);
|
||||
}
|
||||
.modal-x svg {
|
||||
width: 1.125rem;
|
||||
height: 1.125rem;
|
||||
}
|
||||
.modal-body {
|
||||
padding: 0.5rem 1.5rem 0.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.125rem;
|
||||
/* 전역 .modal-body(확인 다이얼로그용)의 가운데 정렬을 폼 모달에선 왼쪽으로 */
|
||||
text-align: left;
|
||||
}
|
||||
.modal-foot {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5625rem;
|
||||
padding: 1rem 1.5rem;
|
||||
border-top: 1px solid var(--border);
|
||||
background: #fafbfc;
|
||||
border-radius: 0 0 0.875rem 0.875rem;
|
||||
}
|
||||
.f-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.f-label {
|
||||
font-size: 0.781rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-2);
|
||||
}
|
||||
.f-input {
|
||||
width: 100%;
|
||||
height: 2.5rem;
|
||||
padding: 0 0.75rem;
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: var(--radius);
|
||||
font-family: inherit;
|
||||
font-size: 0.844rem;
|
||||
color: var(--text);
|
||||
background: #fff;
|
||||
}
|
||||
.f-input::placeholder {
|
||||
color: var(--text-3);
|
||||
}
|
||||
.f-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px var(--accent-weak);
|
||||
}
|
||||
.cat-color {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -284,20 +164,4 @@ onBeforeUnmount(() => document.removeEventListener('keydown', onKey))
|
||||
box-shadow: 0 1px 4px rgba(20, 24, 33, 0.4);
|
||||
cursor: grab;
|
||||
}
|
||||
/* 푸터 버튼 — 전역 .mbtn(확인 다이얼로그용 flex:1) 대신 폼 모달용 우측정렬 컴팩트(멤버 초대 모달과 동일).
|
||||
색/서체/테두리는 전역 .mbtn / .mbtn.primary 가 담당한다. */
|
||||
.mbtn {
|
||||
flex: 0 0 auto;
|
||||
height: 2.375rem;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
.mbtn:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.mbtn.primary:disabled,
|
||||
.mbtn.primary:disabled:hover {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
// 일정 생성/수정 모달
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import BaseModal from '@/components/common/BaseModal.vue'
|
||||
import { useClickOutside } from '@/composables/useClickOutside'
|
||||
import ScheduleAvatar from './ScheduleAvatar.vue'
|
||||
import type {
|
||||
@@ -91,16 +92,9 @@ function togglePerson(id: string): void {
|
||||
? attendeeIds.value.filter((x) => x !== id)
|
||||
: [...attendeeIds.value, id]
|
||||
}
|
||||
// 참석자 멀티셀렉트 바깥클릭 닫기(ESC 는 모달 닫기에서 처리하므로 제외)
|
||||
// 참석자 멀티셀렉트 바깥클릭 닫기(ESC 는 모달 닫기에서 처리)
|
||||
useClickOutside(msOpen, () => (msOpen.value = false), [msRef], { esc: false })
|
||||
|
||||
// 모달 ESC = 닫기
|
||||
function onKey(ev: KeyboardEvent): void {
|
||||
if (ev.key === 'Escape') emit('close')
|
||||
}
|
||||
onMounted(() => document.addEventListener('keydown', onKey))
|
||||
onBeforeUnmount(() => document.removeEventListener('keydown', onKey))
|
||||
|
||||
function submit(): void {
|
||||
if (!valid.value) return
|
||||
const time = endT.value ? `${startT.value} – ${endT.value}` : startT.value
|
||||
@@ -119,373 +113,201 @@ function submit(): void {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="modal-back"
|
||||
@mousedown.self="emit('close')"
|
||||
<BaseModal
|
||||
:title="isEdit ? '일정 수정' : '새 일정'"
|
||||
@close="emit('close')"
|
||||
>
|
||||
<div class="modal">
|
||||
<div class="modal-head">
|
||||
<h3>{{ isEdit ? '일정 수정' : '새 일정' }}</h3>
|
||||
<button
|
||||
class="modal-x"
|
||||
@click="emit('close')"
|
||||
<label class="form-field"><span class="form-label">제목</span>
|
||||
<input
|
||||
v-model="title"
|
||||
class="form-input"
|
||||
placeholder="일정 제목"
|
||||
autofocus
|
||||
>
|
||||
</label>
|
||||
<label class="form-field"><span class="form-label">유형</span>
|
||||
<div class="form-select-wrap">
|
||||
<select
|
||||
v-model="categoryId"
|
||||
class="form-select"
|
||||
>
|
||||
<option
|
||||
v-for="c in ordered"
|
||||
:key="c.id"
|
||||
:value="c.id"
|
||||
>
|
||||
{{ c.label }}
|
||||
</option>
|
||||
</select>
|
||||
<span class="form-select-chev">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.2"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
><path d="M18 6 6 18M6 6l12 12" /></svg>
|
||||
</button>
|
||||
stroke-linejoin="round"
|
||||
><path d="m6 9 6 6 6-6" /></svg>
|
||||
</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<label class="f-field"><span class="f-label">제목</span>
|
||||
<input
|
||||
v-model="title"
|
||||
class="f-input"
|
||||
placeholder="일정 제목"
|
||||
autofocus
|
||||
</label>
|
||||
<div class="form-row">
|
||||
<label class="form-field"><span class="form-label">시작일</span>
|
||||
<input
|
||||
v-model="s"
|
||||
type="date"
|
||||
class="form-input"
|
||||
>
|
||||
</label>
|
||||
<label class="form-field"><span class="form-label">종료일</span>
|
||||
<input
|
||||
v-model="e"
|
||||
type="date"
|
||||
class="form-input"
|
||||
:min="s"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<span class="form-label">시간</span>
|
||||
<div class="time-row">
|
||||
<input
|
||||
v-model="startT"
|
||||
type="time"
|
||||
class="form-input time-input"
|
||||
>
|
||||
<span class="time-dash">–</span>
|
||||
<input
|
||||
v-model="endT"
|
||||
type="time"
|
||||
class="form-input time-input"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<label class="form-field"><span class="form-label">장소</span>
|
||||
<input
|
||||
v-model="place"
|
||||
class="form-input"
|
||||
placeholder="장소 또는 화상"
|
||||
>
|
||||
</label>
|
||||
<div class="form-field">
|
||||
<span class="form-label">참석자</span>
|
||||
<div
|
||||
ref="msRef"
|
||||
class="ms"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="ms-field"
|
||||
:class="{ empty: !allHands && attendeeIds.length === 0, open: msOpen }"
|
||||
@click="msOpen = !msOpen"
|
||||
>
|
||||
<span class="ms-val">{{ msLabel }}</span>
|
||||
<span class="chev">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
><path d="m6 9 6 6 6-6" /></svg>
|
||||
</span>
|
||||
</button>
|
||||
<div
|
||||
v-if="msOpen"
|
||||
class="ms-panel"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="ms-item"
|
||||
:class="{ on: allHands }"
|
||||
@click="toggleAll"
|
||||
>
|
||||
</label>
|
||||
<label class="f-field"><span class="f-label">유형</span>
|
||||
<div class="f-select-wrap">
|
||||
<select
|
||||
v-model="categoryId"
|
||||
class="f-select"
|
||||
<span class="ms-all">전</span>
|
||||
<span class="ms-name">전 직원</span>
|
||||
<span
|
||||
v-if="allHands"
|
||||
class="ms-check"
|
||||
>
|
||||
<option
|
||||
v-for="c in ordered"
|
||||
:key="c.id"
|
||||
:value="c.id"
|
||||
>
|
||||
{{ c.label }}
|
||||
</option>
|
||||
</select>
|
||||
<span class="f-select-chev">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-width="2.4"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
><path d="m6 9 6 6 6-6" /></svg>
|
||||
><path d="M20 6 9 17l-5-5" /></svg>
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
<div class="f-row">
|
||||
<label class="f-field"><span class="f-label">시작일</span>
|
||||
<input
|
||||
v-model="s"
|
||||
type="date"
|
||||
class="f-input"
|
||||
>
|
||||
</label>
|
||||
<label class="f-field"><span class="f-label">종료일</span>
|
||||
<input
|
||||
v-model="e"
|
||||
type="date"
|
||||
class="f-input"
|
||||
:min="s"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
<div class="f-field">
|
||||
<span class="f-label">시간</span>
|
||||
<div class="time-row">
|
||||
<input
|
||||
v-model="startT"
|
||||
type="time"
|
||||
class="f-input time-input"
|
||||
>
|
||||
<span class="time-dash">–</span>
|
||||
<input
|
||||
v-model="endT"
|
||||
type="time"
|
||||
class="f-input time-input"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<label class="f-field"><span class="f-label">장소</span>
|
||||
<input
|
||||
v-model="place"
|
||||
class="f-input"
|
||||
placeholder="장소 또는 화상"
|
||||
</button>
|
||||
<div class="ms-div" />
|
||||
<button
|
||||
v-for="p in people"
|
||||
:key="p.id"
|
||||
type="button"
|
||||
class="ms-item"
|
||||
:class="{ on: attendeeIds.includes(p.id) }"
|
||||
@click="togglePerson(p.id)"
|
||||
>
|
||||
</label>
|
||||
<div class="f-field">
|
||||
<span class="f-label">참석자</span>
|
||||
<ScheduleAvatar
|
||||
:name="p.name"
|
||||
:avatar-url="p.avatarUrl"
|
||||
:size="20"
|
||||
/>
|
||||
<span class="ms-name">{{ p.name }}</span>
|
||||
<span
|
||||
v-if="attendeeIds.includes(p.id)"
|
||||
class="ms-check"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.4"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
><path d="M20 6 9 17l-5-5" /></svg>
|
||||
</span>
|
||||
</button>
|
||||
<div
|
||||
ref="msRef"
|
||||
class="ms"
|
||||
v-if="people.length === 0"
|
||||
class="ms-empty"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="ms-field"
|
||||
:class="{ empty: !allHands && attendeeIds.length === 0, open: msOpen }"
|
||||
@click="msOpen = !msOpen"
|
||||
>
|
||||
<span class="ms-val">{{ msLabel }}</span>
|
||||
<span class="chev">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
><path d="m6 9 6 6 6-6" /></svg>
|
||||
</span>
|
||||
</button>
|
||||
<div
|
||||
v-if="msOpen"
|
||||
class="ms-panel"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="ms-item"
|
||||
:class="{ on: allHands }"
|
||||
@click="toggleAll"
|
||||
>
|
||||
<span class="ms-all">전</span>
|
||||
<span class="ms-name">전 직원</span>
|
||||
<span
|
||||
v-if="allHands"
|
||||
class="ms-check"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.4"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
><path d="M20 6 9 17l-5-5" /></svg>
|
||||
</span>
|
||||
</button>
|
||||
<div class="ms-div" />
|
||||
<button
|
||||
v-for="p in people"
|
||||
:key="p.id"
|
||||
type="button"
|
||||
class="ms-item"
|
||||
:class="{ on: attendeeIds.includes(p.id) }"
|
||||
@click="togglePerson(p.id)"
|
||||
>
|
||||
<ScheduleAvatar
|
||||
:name="p.name"
|
||||
:avatar-url="p.avatarUrl"
|
||||
:size="20"
|
||||
/>
|
||||
<span class="ms-name">{{ p.name }}</span>
|
||||
<span
|
||||
v-if="attendeeIds.includes(p.id)"
|
||||
class="ms-check"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.4"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
><path d="M20 6 9 17l-5-5" /></svg>
|
||||
</span>
|
||||
</button>
|
||||
<div
|
||||
v-if="people.length === 0"
|
||||
class="ms-empty"
|
||||
>
|
||||
등록된 사용자가 없습니다
|
||||
</div>
|
||||
</div>
|
||||
등록된 사용자가 없습니다
|
||||
</div>
|
||||
</div>
|
||||
<label class="f-field"><span class="f-label">설명</span>
|
||||
<textarea
|
||||
v-model="desc"
|
||||
class="f-input f-textarea"
|
||||
placeholder="메모"
|
||||
rows="3"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="modal-foot">
|
||||
<button
|
||||
class="mbtn"
|
||||
@click="emit('close')"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
<button
|
||||
class="mbtn primary"
|
||||
:disabled="!valid"
|
||||
@click="submit"
|
||||
>
|
||||
{{ isEdit ? '저장' : '추가' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<label class="form-field"><span class="form-label">설명</span>
|
||||
<textarea
|
||||
v-model="desc"
|
||||
class="form-input form-textarea"
|
||||
placeholder="메모"
|
||||
rows="3"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<template #foot>
|
||||
<button
|
||||
class="mbtn"
|
||||
@click="emit('close')"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
<button
|
||||
class="mbtn primary"
|
||||
:disabled="!valid"
|
||||
@click="submit"
|
||||
>
|
||||
{{ isEdit ? '저장' : '추가' }}
|
||||
</button>
|
||||
</template>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 사이트 공통 폼 모달 규격(멤버 초대 모달 + relay.css 토큰)에 맞춤 — rem 단위 */
|
||||
.modal-back {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 100;
|
||||
background: rgba(20, 24, 33, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
.modal {
|
||||
width: 100%;
|
||||
max-width: 33.75rem;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
background: #fff;
|
||||
border-radius: 0.875rem;
|
||||
box-shadow: 0 24px 64px rgba(20, 24, 33, 0.32);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.modal-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1.375rem 1.5rem 1rem;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: #fff;
|
||||
border-radius: 0.875rem 0.875rem 0 0;
|
||||
}
|
||||
.modal-head h3 {
|
||||
font-size: 1.0625rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.01875rem;
|
||||
}
|
||||
.modal-x {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-3);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
margin-right: -0.3125rem;
|
||||
}
|
||||
.modal-x:hover {
|
||||
background: #f1f2f4;
|
||||
color: var(--text);
|
||||
}
|
||||
.modal-x svg {
|
||||
width: 1.125rem;
|
||||
height: 1.125rem;
|
||||
}
|
||||
.modal-body {
|
||||
padding: 0.5rem 1.5rem 0.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.125rem;
|
||||
/* 전역 .modal-body(확인 다이얼로그용)의 가운데 정렬을 폼 모달에선 왼쪽으로 */
|
||||
text-align: left;
|
||||
}
|
||||
.modal-foot {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5625rem;
|
||||
padding: 1rem 1.5rem;
|
||||
border-top: 1px solid var(--border);
|
||||
background: #fafbfc;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
border-radius: 0 0 0.875rem 0.875rem;
|
||||
}
|
||||
.f-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.f-label {
|
||||
font-size: 0.781rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-2);
|
||||
}
|
||||
.f-input {
|
||||
width: 100%;
|
||||
height: 2.5rem;
|
||||
padding: 0 0.75rem;
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: var(--radius);
|
||||
font-family: inherit;
|
||||
font-size: 0.844rem;
|
||||
color: var(--text);
|
||||
background: #fff;
|
||||
}
|
||||
.f-input::placeholder {
|
||||
color: var(--text-3);
|
||||
}
|
||||
.f-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px var(--accent-weak);
|
||||
}
|
||||
.f-textarea {
|
||||
height: auto;
|
||||
padding: 0.625rem 0.75rem;
|
||||
resize: vertical;
|
||||
line-height: 1.5;
|
||||
min-height: 5.75rem;
|
||||
}
|
||||
.f-row {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.f-row .f-field {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.f-select-wrap {
|
||||
position: relative;
|
||||
}
|
||||
.f-select {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
height: 2.5rem;
|
||||
padding: 0 2.125rem 0 0.75rem;
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: var(--radius);
|
||||
font-family: inherit;
|
||||
font-size: 0.844rem;
|
||||
color: var(--text);
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
.f-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px var(--accent-weak);
|
||||
}
|
||||
.f-select-chev {
|
||||
position: absolute;
|
||||
right: 0.625rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: grid;
|
||||
color: var(--text-3);
|
||||
pointer-events: none;
|
||||
}
|
||||
.f-select-chev svg {
|
||||
width: 0.9375rem;
|
||||
height: 0.9375rem;
|
||||
}
|
||||
/* 시간 입력 행 */
|
||||
.time-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -499,6 +321,8 @@ function submit(): void {
|
||||
.time-dash {
|
||||
color: var(--text-3);
|
||||
}
|
||||
|
||||
/* 참석자 멀티셀렉트 */
|
||||
.ms {
|
||||
position: relative;
|
||||
}
|
||||
@@ -594,7 +418,7 @@ function submit(): void {
|
||||
height: 0.9375rem;
|
||||
}
|
||||
.ms-all {
|
||||
/* 참석자 아바타(ScheduleAvatar :size="20" = 20px)와 동일 크기로 고정 */
|
||||
/* 참석자 아바타(ScheduleAvatar :size="20" = 20px)와 동일 크기 */
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
@@ -617,20 +441,4 @@ function submit(): void {
|
||||
color: var(--text-3);
|
||||
text-align: center;
|
||||
}
|
||||
/* 푸터 버튼 — 전역 .mbtn(확인 다이얼로그용 flex:1) 대신 폼 모달용 우측정렬 컴팩트(멤버 초대 모달과 동일).
|
||||
색/서체/테두리는 전역 .mbtn / .mbtn.primary 가 담당한다. */
|
||||
.mbtn {
|
||||
flex: 0 0 auto;
|
||||
height: 2.375rem;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
.mbtn:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.mbtn.primary:disabled,
|
||||
.mbtn.primary:disabled:hover {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user