Skip to content

Commit 9fdc334

Browse files
committed
feat: add dialog host
1 parent 2b6d26d commit 9fdc334

File tree

5 files changed

+85
-23
lines changed

5 files changed

+85
-23
lines changed

src/apps/app/app.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { BrowserRouter as Router } from 'react-router-dom';
2+
import { DialogPortal } from '@/shared/utils';
23
import { PostsManagerPage } from '@/pages/home';
34
import { Header } from '@/widgets/header';
45
import { Footer } from '@/widgets/footer';
@@ -13,6 +14,7 @@ export function App() {
1314
</main>
1415
<Footer />
1516
</div>
17+
<DialogPortal />
1618
</Router>
1719
);
1820
}

src/pages/home/ui/post-manager-page.tsx

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export function PostsManagerPage() {
5656
const { tags } = useTags();
5757
const { selectedTag, setTag: setSelectedTag } = useTagFilter(queryParams.get('tag') || '');
5858
const [selectedComment, setSelectedComment] = useState<Comment | null>(null);
59-
// 댓글 입력 상태는 feature dialog 내부에서 관리
59+
6060
const [showAddCommentDialog, setShowAddCommentDialog] = useState(false);
6161
const [showEditCommentDialog, setShowEditCommentDialog] = useState(false);
6262
const postDetail = usePostDetail();
@@ -71,25 +71,16 @@ export function PostsManagerPage() {
7171
sortOrder: (sortOrder as any) || 'asc',
7272
});
7373

74-
// URL 업데이트 함수
7574
const updateURL = () => {
7675
updateUrl(navigate, { skip, limit, search: searchQuery, sortBy, sortOrder, tag: selectedTag });
7776
};
7877

79-
// 게시물 추가/수정은 feature UI에서 처리
80-
81-
// 게시물 삭제 후처리 (feature에서 삭제 수행 후 호출됨)
8278
const onPostDeleted = async (_id: number) => {
8379
void refetch();
8480
};
8581

86-
// 댓글 데이터는 useComments 훅에서 관리
87-
88-
// 댓글 기능 훅 사용
8982
const commentsFeature = useComments(selectedPost?.id ?? null);
90-
// 댓글 추가/수정은 feature dialog에서 처리
9183

92-
// 댓글 삭제
9384
const deleteComment = async (id: number) => {
9485
try {
9586
await commentsFeature.remove(id);
@@ -98,7 +89,6 @@ export function PostsManagerPage() {
9889
}
9990
};
10091

101-
// 댓글 좋아요
10292
const likeComment = async (id: number) => {
10393
try {
10494
await commentsFeature.like(id);
@@ -107,20 +97,16 @@ export function PostsManagerPage() {
10797
}
10898
};
10999

110-
// 게시물 상세 보기
111100
const openPostDetail = (post: Post) => {
112101
setSelectedPost(post);
113102
postDetail.show(post);
114103
};
115104

116-
// 사용자 모달 열기 (feature 사용)
117105
const openUserModal = async (user: User) => {
118106
if (!user?.id) return;
119107
await userModal.show(user.id);
120108
};
121109

122-
// 태그는 useTags 훅에서 초기 로딩
123-
124110
useEffect(() => {
125111
updateURL();
126112
}, [skip, limit, sortBy, sortOrder, selectedTag, searchQuery]);
@@ -149,7 +135,6 @@ export function PostsManagerPage() {
149135
setSkip,
150136
]);
151137

152-
// 게시물 테이블 렌더링 (Feature UI 사용)
153138
const renderPostTable = () => (
154139
<PostTable
155140
posts={posts}
@@ -170,7 +155,6 @@ export function PostsManagerPage() {
170155
/>
171156
);
172157

173-
// 댓글 렌더링
174158
const renderComments = () => (
175159
<div className='mt-2'>
176160
<div className='flex items-center justify-between mb-2'>
@@ -210,7 +194,6 @@ export function PostsManagerPage() {
210194
</Card.Header>
211195
<Card.Content>
212196
<div className='flex flex-col gap-4'>
213-
{/* 검색 및 필터 컨트롤 (Feature UI 사용) */}
214197
<div className='flex gap-4'>
215198
<div className='flex-1'>
216199
<SearchInput value={searchQuery} onChange={setQuery} onEnter={() => void refetch()} />
@@ -231,10 +214,8 @@ export function PostsManagerPage() {
231214
/>
232215
</div>
233216

234-
{/* 게시물 테이블 */}
235217
{loading ? <div className='flex justify-center p-4'>로딩 중...</div> : renderPostTable()}
236218

237-
{/* 페이지네이션 (Feature UI 사용) */}
238219
<PaginationControls
239220
limit={limit}
240221
skip={skip}
@@ -260,7 +241,6 @@ export function PostsManagerPage() {
260241
onSuccess={() => void commentsFeature.refetch()}
261242
/>
262243

263-
{/* 게시물 상세 보기 대화상자 (Feature UI) */}
264244
<PostDetailDialog
265245
open={postDetail.open}
266246
post={postDetail.post}
@@ -272,7 +252,6 @@ export function PostsManagerPage() {
272252
{postDetail.post ? renderComments() : null}
273253
</PostDetailDialog>
274254

275-
{/* Feature Dialogs */}
276255
<AddPostDialog
277256
open={showAddDialog}
278257
onOpenChange={setShowAddDialog}
@@ -285,7 +264,6 @@ export function PostsManagerPage() {
285264
onSuccess={() => void refetch()}
286265
/>
287266

288-
{/* 사용자 모달 (Feature UI 사용) */}
289267
<UserModal
290268
open={userModal.open}
291269
user={userModal.user}

src/shared/utils/dialog/dialog.tsx

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { useSyncExternalStore } from 'react';
2+
import type { ComponentType } from 'react';
3+
4+
type DialogEntry<Props extends object = any> = {
5+
id: number;
6+
Component: ComponentType<Props>;
7+
props: Props;
8+
};
9+
10+
type Listener = () => void;
11+
12+
class DialogStore {
13+
private dialogs: DialogEntry[] = [];
14+
private listeners = new Set<Listener>();
15+
private nextId = 1;
16+
17+
subscribe = (listener: Listener) => {
18+
this.listeners.add(listener);
19+
return () => this.listeners.delete(listener);
20+
};
21+
22+
getSnapshot = () => this.dialogs;
23+
24+
private emit() {
25+
this.listeners.forEach((l) => l());
26+
}
27+
28+
open<Props extends object>(Component: ComponentType<Props>, props: Props) {
29+
const id = this.nextId++;
30+
this.dialogs = [...this.dialogs, { id, Component, props } as DialogEntry];
31+
this.emit();
32+
return id;
33+
}
34+
35+
close(target?: number | ComponentType<any>) {
36+
if (typeof target === 'number') {
37+
this.dialogs = this.dialogs.filter((d) => d.id !== target);
38+
this.emit();
39+
return;
40+
}
41+
if (target) {
42+
this.dialogs = this.dialogs.filter((d) => d.Component !== target);
43+
this.emit();
44+
return;
45+
}
46+
// close all
47+
if (this.dialogs.length > 0) {
48+
this.dialogs = [];
49+
this.emit();
50+
}
51+
}
52+
}
53+
54+
const store = new DialogStore();
55+
56+
export function useDialog() {
57+
return {
58+
open: store.open.bind(store) as <Props extends object>(
59+
Component: ComponentType<Props>,
60+
props: Props,
61+
) => number,
62+
close: store.close.bind(store) as (target?: number | ComponentType<any>) => void,
63+
};
64+
}
65+
66+
function useDialogEntries() {
67+
return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
68+
}
69+
70+
export function DialogPortal() {
71+
const dialogs = useDialogEntries();
72+
return (
73+
<>
74+
{dialogs.map((entry) => {
75+
const Comp = entry.Component as ComponentType<any>;
76+
return <Comp key={entry.id} {...(entry.props as any)} />;
77+
})}
78+
</>
79+
);
80+
}

src/shared/utils/dialog/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './dialog';

src/shared/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './dialog';

0 commit comments

Comments
 (0)