v0.1.18 - 설정 화면과 기간형 D-DAY 관리 추가

This commit is contained in:
2026-04-22 09:47:04 +09:00
parent 4355185203
commit fe538fc88b
13 changed files with 1033 additions and 173 deletions

View File

@@ -0,0 +1,233 @@
<script setup>
const props = defineProps({
goals: {
type: Array,
default: () => [],
},
query: {
type: String,
default: '',
},
status: {
type: String,
default: 'all',
},
form: {
type: Object,
required: true,
},
editingGoalId: {
type: Number,
default: null,
},
busy: {
type: Boolean,
default: false,
},
message: {
type: String,
default: '',
},
selectedDateKey: {
type: String,
default: '',
},
})
const emit = defineEmits([
'update:query',
'update:status',
'update:form-field',
'submit:create',
'start-edit',
'cancel-edit',
'submit:update',
])
function updateField(field, event) {
emit('update:form-field', {
field,
value: event.target.value,
})
}
function isActiveOnSelectedDate(goal) {
if (!goal.activeFrom || !goal.activeUntil || !props.selectedDateKey) {
return false
}
return props.selectedDateKey >= goal.activeFrom && props.selectedDateKey <= goal.activeUntil
}
</script>
<template>
<section class="grid gap-6 xl:grid-cols-[380px_minmax(0,1fr)]">
<form class="rounded-[28px] border border-white/60 bg-white/75 p-6" @submit.prevent="emit(editingGoalId ? 'submit:update' : 'submit:create')">
<p class="text-[11px] font-bold uppercase tracking-[0.24em] text-stone-500">
{{ editingGoalId ? 'Edit Goal' : 'Create Goal' }}
</p>
<div class="mt-5 space-y-4">
<div class="space-y-2">
<label class="text-[11px] font-bold tracking-[0.16em] text-stone-600">목표 이름</label>
<input
:value="form.title"
type="text"
class="w-full rounded-2xl border border-stone-300 bg-white px-4 py-3 text-sm font-semibold text-stone-800 outline-none transition focus:border-stone-500"
placeholder="예: 자격증 시험 / 프로젝트 런칭 / 운동 루틴"
@input="updateField('title', $event)"
/>
</div>
<div class="space-y-2">
<label class="text-[11px] font-bold tracking-[0.16em] text-stone-600">목표일</label>
<input
:value="form.targetDate"
type="date"
class="w-full rounded-2xl border border-stone-300 bg-white px-4 py-3 text-sm font-semibold text-stone-800 outline-none transition focus:border-stone-500"
@input="updateField('targetDate', $event)"
/>
</div>
<div class="grid gap-4 sm:grid-cols-2">
<div class="space-y-2">
<label class="text-[11px] font-bold tracking-[0.16em] text-stone-600">표시 시작일</label>
<input
:value="form.activeFrom"
type="date"
class="w-full rounded-2xl border border-stone-300 bg-white px-4 py-3 text-sm font-semibold text-stone-800 outline-none transition focus:border-stone-500"
@input="updateField('activeFrom', $event)"
/>
</div>
<div class="space-y-2">
<label class="text-[11px] font-bold tracking-[0.16em] text-stone-600">표시 종료일</label>
<input
:value="form.activeUntil"
type="date"
class="w-full rounded-2xl border border-stone-300 bg-white px-4 py-3 text-sm font-semibold text-stone-800 outline-none transition focus:border-stone-500"
@input="updateField('activeUntil', $event)"
/>
</div>
</div>
<div class="space-y-2">
<label class="text-[11px] font-bold tracking-[0.16em] text-stone-600">상태</label>
<select
:value="form.status"
class="w-full rounded-2xl border border-stone-300 bg-white px-4 py-3 text-sm font-semibold text-stone-800 outline-none transition focus:border-stone-500"
@change="updateField('status', $event)"
>
<option value="active">진행 </option>
<option value="done">완료</option>
<option value="archived">보관</option>
</select>
</div>
<p class="rounded-2xl border border-stone-200 bg-[#fbf7f0] px-4 py-3 text-[11px] font-semibold leading-5 tracking-[0.06em] text-stone-600">
여기서 목표와 표시 기간을 설정해 두면, 플래너 작성 화면에서는 해당 날짜에 보여줄지 여부만 간단히 ON/OFF 있습니다.
</p>
<p
v-if="message"
class="rounded-2xl border border-stone-300 bg-white/80 px-4 py-3 text-sm font-semibold leading-6 text-stone-700"
>
{{ message }}
</p>
<div class="flex flex-wrap gap-2">
<button
type="submit"
class="rounded-full bg-stone-900 px-5 py-3 text-xs font-bold tracking-[0.18em] text-white transition hover:bg-stone-700 disabled:cursor-not-allowed disabled:bg-stone-400"
:disabled="busy"
>
{{ busy ? '저장 중...' : editingGoalId ? '목표 수정' : '목표 추가' }}
</button>
<button
v-if="editingGoalId"
type="button"
class="rounded-full border border-stone-300 px-5 py-3 text-xs font-bold tracking-[0.18em] text-stone-600 transition hover:border-stone-500 hover:text-stone-900"
@click="emit('cancel-edit')"
>
취소
</button>
</div>
</div>
</form>
<section class="rounded-[28px] border border-white/60 bg-white/75 p-6">
<div class="flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
<div>
<p class="text-[11px] font-bold uppercase tracking-[0.24em] text-stone-500">Goal Library</p>
<p class="mt-2 text-sm font-semibold leading-6 text-stone-600">
목표가 많아져도 플래너 작성 화면이 길어지지 않도록, 전체 관리는 화면에서 처리합니다.
</p>
</div>
<div class="grid gap-2 sm:grid-cols-[220px_140px]">
<input
:value="query"
type="text"
class="rounded-2xl border border-stone-300 bg-white px-4 py-3 text-sm font-semibold text-stone-800 outline-none transition focus:border-stone-500"
placeholder="목표 검색"
@input="emit('update:query', $event.target.value)"
/>
<select
:value="status"
class="rounded-2xl border border-stone-300 bg-white px-4 py-3 text-sm font-semibold text-stone-800 outline-none transition focus:border-stone-500"
@change="emit('update:status', $event.target.value)"
>
<option value="all">전체</option>
<option value="active">진행 </option>
<option value="done">완료</option>
<option value="archived">보관</option>
</select>
</div>
</div>
<div class="mt-5 grid gap-4">
<article
v-for="goal in goals"
:key="goal.id"
class="rounded-[24px] border border-stone-200 bg-white px-5 py-5"
>
<div class="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
<div class="space-y-2">
<div class="flex flex-wrap items-center gap-2">
<p class="text-lg font-semibold tracking-[-0.03em] text-stone-900">{{ goal.title }}</p>
<span
class="rounded-full px-3 py-1 text-[10px] font-bold tracking-[0.16em]"
:class="goal.status === 'done' ? 'bg-emerald-100 text-emerald-700' : goal.status === 'archived' ? 'bg-stone-200 text-stone-600' : 'bg-stone-900 text-white'"
>
{{ goal.status === 'done' ? '완료' : goal.status === 'archived' ? '보관' : '진행 중' }}
</span>
<span
v-if="isActiveOnSelectedDate(goal)"
class="rounded-full bg-amber-100 px-3 py-1 text-[10px] font-bold tracking-[0.16em] text-amber-700"
>
현재 날짜에 표시
</span>
</div>
<p class="text-sm font-semibold text-stone-600">목표일 {{ goal.targetDate }}</p>
<p class="text-[11px] font-semibold tracking-[0.06em] text-stone-500">
{{ goal.activeFrom && goal.activeUntil ? `표시 기간 ${goal.activeFrom} ~ ${goal.activeUntil}` : '표시 기간 미설정' }}
</p>
</div>
<button
type="button"
class="rounded-full border border-stone-300 px-4 py-2 text-[11px] font-bold tracking-[0.16em] text-stone-600 transition hover:border-stone-500 hover:text-stone-900"
@click="emit('start-edit', goal)"
>
수정
</button>
</div>
</article>
<div
v-if="goals.length === 0"
class="rounded-[24px] border border-dashed border-stone-300 bg-white px-5 py-8 text-center"
>
<p class="text-sm font-semibold text-stone-600">조건에 맞는 목표가 없습니다.</p>
</div>
</div>
</section>
</section>
</template>

View File

@@ -0,0 +1,171 @@
<script setup>
import { computed } from 'vue'
const props = defineProps({
user: {
type: Object,
required: true,
},
profileForm: {
type: Object,
required: true,
},
passwordForm: {
type: Object,
required: true,
},
profileBusy: {
type: Boolean,
default: false,
},
passwordBusy: {
type: Boolean,
default: false,
},
profileMessage: {
type: String,
default: '',
},
passwordMessage: {
type: String,
default: '',
},
})
const emit = defineEmits([
'update:profile-field',
'update:password-field',
'submit:profile',
'submit:password',
])
const initials = computed(() =>
`${props.user.nickname?.slice(0, 1) ?? ''}${props.user.email?.slice(0, 1) ?? ''}`.toUpperCase(),
)
function updateProfileField(field, event) {
emit('update:profile-field', {
field,
value: event.target.value,
})
}
function updatePasswordField(field, event) {
emit('update:password-field', {
field,
value: event.target.value,
})
}
</script>
<template>
<section class="grid gap-6 xl:grid-cols-[360px_minmax(0,1fr)]">
<aside class="rounded-[28px] border border-white/60 bg-white/70 p-6">
<p class="text-[11px] font-bold uppercase tracking-[0.28em] text-stone-500">Settings</p>
<div class="mt-6 flex items-center gap-4">
<div class="flex h-20 w-20 items-center justify-center rounded-full bg-stone-900 text-2xl font-bold tracking-[0.04em] text-white">
{{ initials || 'U' }}
</div>
<div>
<p class="text-xl font-semibold tracking-[-0.04em] text-stone-900">{{ user.nickname }}</p>
<p class="mt-1 text-sm font-semibold text-stone-500">{{ user.email }}</p>
</div>
</div>
<div class="mt-6 space-y-3 rounded-[24px] border border-stone-200 bg-[#fbf7f0] p-4">
<p class="text-[10px] font-bold tracking-[0.18em] text-stone-500">PROFILE NOTE</p>
<p class="text-sm font-semibold leading-6 text-stone-700">
썸네일 이미지는 다음 단계에서 붙이는 편이 자연스럽습니다. 이번 단계에서는 계정 정보 수정과 비밀번호 변경 흐름을 먼저 안정화합니다.
</p>
</div>
</aside>
<div class="grid gap-6">
<form class="rounded-[28px] border border-white/60 bg-white/75 p-6" @submit.prevent="emit('submit:profile')">
<p class="text-[11px] font-bold uppercase tracking-[0.24em] text-stone-500">Account Profile</p>
<div class="mt-5 grid gap-4 md:grid-cols-2">
<div class="space-y-2">
<label class="text-[11px] font-bold tracking-[0.16em] text-stone-600">닉네임</label>
<input
:value="profileForm.nickname"
type="text"
class="w-full rounded-2xl border border-stone-300 bg-white px-4 py-3 text-sm font-semibold text-stone-800 outline-none transition focus:border-stone-500"
@input="updateProfileField('nickname', $event)"
/>
</div>
<div class="space-y-2">
<label class="text-[11px] font-bold tracking-[0.16em] text-stone-600">이메일</label>
<input
:value="profileForm.email"
type="email"
class="w-full rounded-2xl border border-stone-300 bg-white px-4 py-3 text-sm font-semibold text-stone-800 outline-none transition focus:border-stone-500"
@input="updateProfileField('email', $event)"
/>
</div>
</div>
<p
v-if="profileMessage"
class="mt-4 rounded-2xl border border-stone-300 bg-white/80 px-4 py-3 text-sm font-semibold leading-6 text-stone-700"
>
{{ profileMessage }}
</p>
<button
type="submit"
class="mt-5 rounded-full bg-stone-900 px-5 py-3 text-xs font-bold tracking-[0.18em] text-white transition hover:bg-stone-700 disabled:cursor-not-allowed disabled:bg-stone-400"
:disabled="profileBusy"
>
{{ profileBusy ? '저장 중...' : '프로필 저장' }}
</button>
</form>
<form class="rounded-[28px] border border-white/60 bg-white/75 p-6" @submit.prevent="emit('submit:password')">
<p class="text-[11px] font-bold uppercase tracking-[0.24em] text-stone-500">Password</p>
<div class="mt-5 grid gap-4 md:grid-cols-2">
<div class="space-y-2 md:col-span-2">
<label class="text-[11px] font-bold tracking-[0.16em] text-stone-600">현재 비밀번호</label>
<input
:value="passwordForm.currentPassword"
type="password"
class="w-full rounded-2xl border border-stone-300 bg-white px-4 py-3 text-sm font-semibold text-stone-800 outline-none transition focus:border-stone-500"
@input="updatePasswordField('currentPassword', $event)"
/>
</div>
<div class="space-y-2">
<label class="text-[11px] font-bold tracking-[0.16em] text-stone-600"> 비밀번호</label>
<input
:value="passwordForm.newPassword"
type="password"
class="w-full rounded-2xl border border-stone-300 bg-white px-4 py-3 text-sm font-semibold text-stone-800 outline-none transition focus:border-stone-500"
@input="updatePasswordField('newPassword', $event)"
/>
</div>
<div class="space-y-2">
<label class="text-[11px] font-bold tracking-[0.16em] text-stone-600"> 비밀번호 확인</label>
<input
:value="passwordForm.confirmPassword"
type="password"
class="w-full rounded-2xl border border-stone-300 bg-white px-4 py-3 text-sm font-semibold text-stone-800 outline-none transition focus:border-stone-500"
@input="updatePasswordField('confirmPassword', $event)"
/>
</div>
</div>
<p
v-if="passwordMessage"
class="mt-4 rounded-2xl border border-stone-300 bg-white/80 px-4 py-3 text-sm font-semibold leading-6 text-stone-700"
>
{{ passwordMessage }}
</p>
<button
type="submit"
class="mt-5 rounded-full border border-stone-900 px-5 py-3 text-xs font-bold tracking-[0.18em] text-stone-900 transition hover:bg-stone-900 hover:text-white disabled:cursor-not-allowed disabled:border-stone-300 disabled:text-stone-400"
:disabled="passwordBusy"
>
{{ passwordBusy ? '변경 중...' : '비밀번호 변경' }}
</button>
</form>
</div>
</section>
</template>