Skip to content

Commit ac4a954

Browse files
committed
pinnedUntil, importantUntil
1 parent c4111fa commit ac4a954

File tree

10 files changed

+149
-13
lines changed

10 files changed

+149
-13
lines changed

app/routes/community/news/components/NewsEditor.tsx

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { FormProvider, useForm } from 'react-hook-form';
1+
import { FormProvider, useForm, useWatch } from 'react-hook-form';
22

33
import Fieldset from '~/components/form/Fieldset';
44
import Form from '~/components/form/Form';
@@ -15,6 +15,8 @@ export interface NewsFormData {
1515
tags: string[];
1616
isPrivate: boolean;
1717
isImportant: boolean;
18+
importantUntil: Date | null;
19+
hasImportantUntilDeadline: boolean;
1820
isSlide: boolean;
1921
}
2022

@@ -42,11 +44,16 @@ export default function NewsEditor({
4244
tags: [],
4345
isPrivate: false,
4446
isImportant: false,
47+
importantUntil: null,
4548
isSlide: false,
4649
},
4750
shouldFocusError: false,
4851
});
4952
const { handleSubmit, setValue } = formMethods;
53+
const [isImportant, importantUntil] = useWatch({
54+
name: ['isImportant', 'importantUntil'],
55+
control: formMethods.control,
56+
});
5057

5158
return (
5259
<FormProvider {...formMethods}>
@@ -113,8 +120,36 @@ export default function NewsEditor({
113120
name="isImportant"
114121
onChange={(isImportant) => {
115122
if (isImportant) setValue('isPrivate', false);
123+
setValue('importantUntil', null);
124+
setValue('hasImportantUntilDeadline', false);
116125
}}
117126
/>
127+
{isImportant && (
128+
<div className="ml-6 flex flex-col gap-2">
129+
<Form.Checkbox
130+
label="만료일 설정"
131+
name="hasImportantUntilDeadline"
132+
onChange={(checked) => {
133+
if (checked) {
134+
const tomorrow = new Date();
135+
tomorrow.setDate(tomorrow.getDate() + 1);
136+
tomorrow.setHours(0, 0, 0, 0);
137+
setValue('importantUntil', tomorrow);
138+
} else {
139+
setValue('importantUntil', null);
140+
}
141+
}}
142+
/>
143+
{importantUntil && (
144+
<div className="ml-6">
145+
<Form.Date name="importantUntil" hideTime disablePast />
146+
</div>
147+
)}
148+
<p className="text-xs font-light tracking-wide text-neutral-700">
149+
* 만료일 설정 시 해당 날짜까지만 중요 안내로 표시됩니다.
150+
</p>
151+
</div>
152+
)}
118153
<Form.Checkbox
119154
label="메인-슬라이드쇼에 표시"
120155
name="isSlide"

app/routes/community/news/create.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import dayjs from 'dayjs';
12
import { useNavigate } from 'react-router';
23
import { toast } from 'sonner';
34
import PageLayout from '~/components/layout/PageLayout';
@@ -21,10 +22,12 @@ export default function NewsCreatePage() {
2122
title: content.title,
2223
titleForMain: content.titleForMain || null,
2324
description: content.description,
24-
// TODO: 정확한 date format이?
25-
date: content.date,
25+
date: dayjs(content.date).format('YYYY-MM-DD'),
2626
isPrivate: content.isPrivate,
2727
isImportant: content.isImportant,
28+
importantUntil: content.importantUntil
29+
? dayjs(content.importantUntil).format('YYYY-MM-DD')
30+
: null,
2831
isSlide: content.isSlide,
2932
tags: content.tags,
3033
});

app/routes/community/news/edit.$id.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Route } from '.react-router/types/app/routes/community/news/+types/edit.$id';
2+
import dayjs from 'dayjs';
23
import type { LoaderFunctionArgs } from 'react-router';
34
import { useNavigate } from 'react-router';
45
import { toast } from 'sonner';
@@ -24,7 +25,7 @@ export default function NewsEditPage({ loaderData }: Route.ComponentProps) {
2425
title: data.title,
2526
titleForMain: data.titleForMain ?? '',
2627
description: data.description,
27-
date: new Date(data.date),
28+
date: dayjs(data.date, 'YYYY-MM-DD').toDate(),
2829
image: data.imageURL
2930
? { type: 'UPLOADED_IMAGE', url: data.imageURL }
3031
: null,
@@ -35,6 +36,10 @@ export default function NewsEditPage({ loaderData }: Route.ComponentProps) {
3536
tags: data.tags,
3637
isPrivate: data.isPrivate,
3738
isImportant: data.isImportant,
39+
importantUntil: data.importantUntil
40+
? dayjs(data.importantUntil, 'YYYY-MM-DD').toDate()
41+
: null,
42+
hasImportantUntilDeadline: data.importantUntil !== null,
3843
isSlide: data.isSlide,
3944
};
4045

@@ -54,10 +59,10 @@ export default function NewsEditPage({ loaderData }: Route.ComponentProps) {
5459
title: content.title,
5560
titleForMain: content.titleForMain || null,
5661
description: content.description,
57-
// TODO: 정확한 date format이?
58-
date: content.date,
62+
date: content.date.toISOString(),
5963
isPrivate: content.isPrivate,
6064
isImportant: content.isImportant,
65+
importantUntil: content.importantUntil?.toISOString() ?? null,
6166
isSlide: content.isSlide,
6267
tags: content.tags,
6368
deleteIds,

app/routes/community/notice/components/NoticeEditor.tsx

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { FormProvider, useForm } from 'react-hook-form';
1+
import { FormProvider, useForm, useWatch } from 'react-hook-form';
22

33
import Fieldset from '~/components/form/Fieldset';
44
import Form from '~/components/form/Form';
@@ -13,7 +13,11 @@ export interface NoticeFormData {
1313
tags: string[];
1414
isPrivate: boolean;
1515
isPinned: boolean;
16+
pinnedUntil: Date | null;
17+
hasPinnedUntilDeadline: boolean;
1618
isImportant: boolean;
19+
importantUntil: Date | null;
20+
hasImportantUntilDeadline: boolean;
1721
}
1822

1923
interface Props {
@@ -38,11 +42,17 @@ export default function NoticeEditor({
3842
tags: [],
3943
isPrivate: false,
4044
isPinned: false,
45+
pinnedUntil: null,
4146
isImportant: false,
47+
importantUntil: null,
4248
},
4349
shouldFocusError: false,
4450
});
4551
const { handleSubmit, setValue } = formMethods;
52+
const [isPinned, pinnedUntil, isImportant, importantUntil] = useWatch({
53+
name: ['isPinned', 'pinnedUntil', 'isImportant', 'importantUntil'],
54+
control: formMethods.control,
55+
});
4656

4757
return (
4858
<FormProvider {...formMethods}>
@@ -97,15 +107,71 @@ export default function NoticeEditor({
97107
name="isPinned"
98108
onChange={(isPinned) => {
99109
if (isPinned) setValue('isPrivate', false);
110+
setValue('pinnedUntil', null);
111+
setValue('hasPinnedUntilDeadline', false);
100112
}}
101113
/>
114+
{isPinned && (
115+
<div className="ml-6 flex flex-col gap-2">
116+
<Form.Checkbox
117+
label="만료일 설정"
118+
name="hasPinnedUntilDeadline"
119+
onChange={(checked) => {
120+
if (checked) {
121+
const tomorrow = new Date();
122+
tomorrow.setDate(tomorrow.getDate() + 1);
123+
tomorrow.setHours(0, 0, 0, 0);
124+
setValue('pinnedUntil', tomorrow);
125+
} else {
126+
setValue('pinnedUntil', null);
127+
}
128+
}}
129+
/>
130+
{pinnedUntil && (
131+
<div className="ml-6">
132+
<Form.Date name="pinnedUntil" hideTime disablePast />
133+
</div>
134+
)}
135+
<p className="text-xs font-light tracking-wide text-neutral-700">
136+
* 만료일 설정 시 해당 날짜까지만 상단에 고정됩니다.
137+
</p>
138+
</div>
139+
)}
102140
<Form.Checkbox
103141
label="메인-중요 안내에 표시"
104142
name="isImportant"
105143
onChange={(isImportant) => {
106144
if (isImportant) setValue('isPrivate', false);
145+
setValue('importantUntil', null);
146+
setValue('hasImportantUntilDeadline', false);
107147
}}
108148
/>
149+
{isImportant && (
150+
<div className="ml-6 flex flex-col gap-2">
151+
<Form.Checkbox
152+
label="만료일 설정"
153+
name="hasImportantUntilDeadline"
154+
onChange={(checked) => {
155+
if (checked) {
156+
const tomorrow = new Date();
157+
tomorrow.setDate(tomorrow.getDate() + 1);
158+
tomorrow.setHours(0, 0, 0, 0);
159+
setValue('importantUntil', tomorrow);
160+
} else {
161+
setValue('importantUntil', null);
162+
}
163+
}}
164+
/>
165+
{importantUntil && (
166+
<div className="ml-6">
167+
<Form.Date name="importantUntil" hideTime disablePast />
168+
</div>
169+
)}
170+
<p className="text-xs font-light tracking-wide text-neutral-700">
171+
* 만료일 설정 시 해당 날짜까지만 중요 안내로 표시됩니다.
172+
</p>
173+
</div>
174+
)}
109175
</div>
110176
</Fieldset>
111177
<Form.Action

app/routes/community/notice/create.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import dayjs from 'dayjs';
12
import { useNavigate } from 'react-router';
23
import { toast } from 'sonner';
34
import PageLayout from '~/components/layout/PageLayout';
@@ -23,7 +24,12 @@ export default function NoticeCreatePage() {
2324
description: content.description,
2425
isPrivate: content.isPrivate,
2526
isPinned: content.isPinned,
27+
pinnedUntil: content.pinnedUntil
28+
? dayjs(content.pinnedUntil).format('YYYY-MM-DD')
29+
: null,
2630
isImportant: content.isImportant,
31+
importantUntil:
32+
dayjs(content.importantUntil).format('YYYY-MM-DD') ?? null,
2733
tags: content.tags,
2834
});
2935

app/routes/community/notice/edit.$id.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import type { Route } from '.react-router/types/app/routes/community/notice/+types/edit.$id';
2+
import dayjs from 'dayjs';
3+
import customParseFormat from 'dayjs/plugin/customParseFormat';
24
import type { LoaderFunctionArgs } from 'react-router';
35
import { useNavigate } from 'react-router';
46
import { toast } from 'sonner';
@@ -10,6 +12,8 @@ import { fetchJson, fetchOk } from '~/utils/fetch';
1012
import { FormData2, getDeleteIds } from '~/utils/form';
1113
import NoticeEditor, { type NoticeFormData } from './components/NoticeEditor';
1214

15+
dayjs.extend(customParseFormat);
16+
1317
export async function loader({ params }: LoaderFunctionArgs) {
1418
const id = Number(params.id);
1519
const data = await fetchJson<Notice>(`${BASE_URL}/v2/notice/${id}`);
@@ -30,8 +34,16 @@ export default function NoticeEditPage({ loaderData }: Route.ComponentProps) {
3034
})),
3135
tags: data.tags,
3236
isPrivate: data.isPrivate,
33-
isImportant: data.isImportant,
3437
isPinned: data.isPinned,
38+
pinnedUntil: data.pinnedUntil
39+
? dayjs(data.pinnedUntil, 'YYYY-MM-DD').toDate()
40+
: null,
41+
hasPinnedUntilDeadline: data.pinnedUntil !== null,
42+
isImportant: data.isImportant,
43+
importantUntil: data.importantUntil
44+
? dayjs(data.importantUntil, 'YYYY-MM-DD').toDate()
45+
: null,
46+
hasImportantUntilDeadline: data.importantUntil !== null,
3547
};
3648

3749
const onCancel = () => {
@@ -52,7 +64,13 @@ export default function NoticeEditPage({ loaderData }: Route.ComponentProps) {
5264
description: content.description,
5365
isPrivate: content.isPrivate,
5466
isPinned: content.isPinned,
67+
pinnedUntil: content.pinnedUntil
68+
? dayjs(content.pinnedUntil).format('YYYY-MM-DD')
69+
: null,
5570
isImportant: content.isImportant,
71+
importantUntil: content.importantUntil
72+
? dayjs(content.importantUntil).format('YYYY-MM-DD')
73+
: null,
5674
tags: content.tags,
5775
deleteIds,
5876
});

app/routes/layout.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import type { Route } from '.react-router/types/app/routes/+types/layout';
2+
import dayjs from 'dayjs';
23
import { useEffect } from 'react';
34
import { isRouteErrorResponse, Outlet, useNavigate } from 'react-router';
45
import { Toaster } from 'sonner';
5-
import ErrorState from '~/components/ui/ErrorState';
66
import Header from '~/components/layout/Header';
7+
import ErrorState from '~/components/ui/ErrorState';
78
import { BASE_URL } from '~/constants/api';
89
import { useLanguage } from '~/hooks/useLanguage';
910
import { type Role, useStore } from '~/store';
1011

12+
// TODO: 필요한가?
13+
dayjs.locale('ko');
14+
1115
export async function loader({
1216
request,
1317
}: Route.LoaderArgs): Promise<Role | undefined> {

app/types/api/v2/news/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export interface News {
2323
imageURL: string | null;
2424
isSlide: boolean;
2525
isImportant: boolean;
26+
importantUntil: string | null;
2627
id: number;
2728
createdAt: string;
2829
modifiedAt: string;

app/types/api/v2/notice/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ export interface Notice {
1919
isPrivate: boolean;
2020
tags: string[];
2121
isPinned: boolean;
22+
pinnedUntil: string | null;
2223
isImportant: boolean;
24+
importantUntil: string | null;
2325
author: string;
2426
id: number;
2527
createdAt: string;

react-router.config.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,4 @@ export default {
44
// Config options...
55
// Server-side render by default, to enable SPA mode set this to `false`
66
ssr: true,
7-
prerender: {
8-
paths: true,
9-
unstable_concurrency: 4,
10-
},
117
} satisfies Config;

0 commit comments

Comments
 (0)