From 86bbd8d1c8fa8fc6325fd21f7bef51745146806b Mon Sep 17 00:00:00 2001 From: MINSEONG KIM Date: Wed, 28 May 2025 01:29:03 +0900 Subject: [PATCH] =?UTF-8?q?[Fix]=20Suspense=20Boundary=20=EC=A1=B0?= =?UTF-8?q?=EC=A0=88=EC=9D=84=20=ED=86=B5=ED=95=9C=20=EC=84=B1=EB=8A=A5=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=20(#188)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(apps/web): 오전/오후에 따른 시간 포멧팅 * fix(apps/web): form id 연동 * feat(apps/web): 주제 생성 페이지 streaming ssr 적용 * feat(apps/web): 업로드 예약 일정 페이지 streaming ssr 적용 * feat(apps/web): 결과 수정 페이지 streaming ssr 적용 * fix(apps/web): 조건문 수정 * feat(apps/web): 업로드 예약 일정 상세 페이지 streaming ssr 적용 * fix(apps/web): client side에서 prefetch로 변경 * chore(apps/web): UPLOAD_CONFIRMED 상태 추가 * feat(apps/web): 개인화 설정 페이지 streaming ssr 적용 * feat(apps/web): 홈 페이지 streaming ssr 적용 * remove(apps/web): 불필요 코드 제거 * feat(apps/web): 업로드 예약 일정 페이지 streaming ssr 적용 * fix(apps/web): Number 강제 타입 변환 * feat(apps/web): 결과 수정 상세 페이지 서버 패칭 및 streaming ssr 적용, provider 분리 * fix(apps/web): 업로드 시 UPLOAD_CONFIRMED로 상태 변경 * fix(apps/web): statusPriority 변경 * chore(apps/web): 불필요 export 제거 * remove(apps/web): Loading 제거 * fix(apps/web): 버그 수정 * chore(apps/web): v1 api로 변경 * fix(apps/web): 업로드 예약 일정 페이지 버그 개선 * chore(apps/web): 파일, 함수명 오타 수정 * fix(apps/web): usePrefetchNewsCategories 변경 * fix(apps/web): 로그아웃 모달 수정 * fix(packages/ui): zIndex 설정 --- apps/web/src/app/(home)/Home.tsx | 85 +------ apps/web/src/app/(home)/[agentId]/Home.tsx | 182 +++------------ .../AgentDetailPersonalCard.tsx | 23 ++ .../ContentGroupCard/ContentGroupCard.css.ts | 1 + .../ContentGroupCard/ContentGroupCard.tsx | 4 +- .../ContentGroupCardSkeleton.tsx | 35 +++ .../_components/PersonalCard/PersonalCard.tsx | 7 +- .../PersonalCard/PersonalCardSkeleton.tsx | 27 +++ .../PostGroupsContentGroupCard.tsx | 60 +++++ .../ReservedUploadContentCard.tsx | 37 +++ .../UploadContentCard.css.ts | 6 + .../UploadContentCard/UploadContentCard.tsx | 4 +- .../UploadContentCardSkeleton.tsx | 42 ++++ apps/web/src/app/(home)/[agentId]/page.tsx | 16 +- apps/web/src/app/(home)/page.tsx | 5 +- .../edit/[agentId]/[postGroupId]/Edit.tsx | 104 +++------ .../EditContent/EditContentSkeleton.tsx | 84 +++++++ .../EditContent/EditContentWithDND.tsx | 57 +++++ .../_components/EditContent/index.ts | 2 + .../[postGroupId]/detail/EditDetail.tsx | 23 +- .../detail/_components/EditPost/EditPost.tsx | 5 +- .../EditPromptField/EditPromptField.tsx | 4 +- .../_components/EditSidebar/EditSidebar.tsx | 4 +- .../_components/PostEditor/PostEditor.tsx | 5 +- .../detail/_hooks/useAdjacentPosts.tsx | 5 +- .../_providers/EditDetailPageProvider.tsx | 37 +++ .../[agentId]/[postGroupId]/detail/page.tsx | 24 +- .../[agentId]/[postGroupId]/detail/type.ts | 5 + .../[postGroupId]/log/[postId]/Log.tsx | 3 - .../LogContentItem/LogContentItem.css.ts | 23 -- .../LogContentItem/LogContentItem.tsx | 55 ----- .../_components/LogSidebar/LogSidebar.css.ts | 15 -- .../_components/LogSidebar/LogSidebar.tsx | 27 --- .../[postGroupId]/log/[postId]/page.tsx | 26 --- .../edit/[agentId]/[postGroupId]/page.tsx | 10 +- .../[postGroupId]/schedule/Schedule.tsx | 138 +++++------ .../ReadyToUploadPostsLength.tsx | 5 + .../ScheduleContent/ScheduleContent.tsx | 69 ++++++ .../ScheduleContentSkeleton.tsx | 28 +++ .../_components/ScheduleContent/style.css.ts | 36 +++ .../SubmitBottomCTA/SubmitBottomCTA.tsx | 43 ++++ .../SubmitBottomCTASkeleton.tsx | 12 + .../_components/SubmitBottomCTA/index.ts | 2 + .../[agentId]/[postGroupId]/schedule/page.tsx | 10 +- .../edit/[agentId]/[postGroupId]/types.ts | 10 + apps/web/src/app/create/[agentId]/Create.tsx | 40 ++-- .../NewsCategorySection.tsx | 31 +++ .../NewsCategorySectionSkeleton.tsx | 14 ++ .../_components/NewsCategorySection/index.ts | 2 + .../NewsCategorySection/style.css.ts | 9 + apps/web/src/app/create/[agentId]/page.tsx | 15 +- apps/web/src/app/loading.tsx | 17 -- .../app/personalize/[agentId]/Personalize.tsx | 218 +++--------------- .../PersonalizeFormContent.tsx | 118 ++++++++++ .../PersonalizeFormContentSkeleton.tsx | 29 +++ .../PersonalizeFormContent/style.css.ts | 23 ++ .../src/app/personalize/[agentId]/page.tsx | 6 +- .../src/app/schedule/[agentId]/Schedule.tsx | 201 +++------------- .../[postGroupId]/[postId]/ScheduleDetail.tsx | 83 ++----- .../BreadcrumbContent/BreadcrumbContent.tsx | 24 ++ .../BreadcrumbItemContentSkelton.tsx | 5 + .../BreadcrumbContent/style.css.ts | 8 + .../ScheduleDetailContent.tsx | 58 +++++ .../ScheduleDetailContentSkeleton.tsx | 21 ++ .../ScheduleDetailContent/index.ts | 2 + .../ScheduleDetailContent/style.css.ts | 29 +++ .../[agentId]/[postGroupId]/[postId]/page.tsx | 16 +- .../[postGroupId]/[postId]/pageStyle.css.ts | 29 +-- .../ScheduleContent/ScheduleContent.tsx | 116 ++++++++++ .../ScheduleContentSkeleton.tsx | 28 +++ .../_components/ScheduleContent/style.css.ts | 55 +++++ apps/web/src/app/schedule/[agentId]/page.tsx | 11 +- apps/web/src/app/schedule/[agentId]/type.ts | 9 + .../AccountSidebar/AccountSidebar.css.ts | 24 +- .../common/AccountSidebar/AccountSidebar.tsx | 68 +++--- .../AccountSidebar/AccountSidebarSkeleton.tsx | 24 ++ .../BreadcrumbContent/BreadcrumbContent.tsx | 24 ++ .../BreadcrumbItemContentSkelton.tsx | 5 + .../common/BreadcrumbContent/style.css.ts | 8 + .../TitleWithDescription.tsx | 3 +- .../UserProfileDropdown.css.ts | 14 ++ .../UserProfileDropdown.tsx | 95 ++++++++ .../UserProfileDropdown/assets/iconNotice.png | Bin 0 -> 5724 bytes apps/web/src/components/common/index.ts | 6 +- .../schedule/ScheduleTable/ScheduleTable.tsx | 30 +-- .../schedule/ScheduleTable/constants.ts | 29 +++ .../mutation/useCreateMorePostsMutation.ts | 7 +- .../store/mutation/useCreatePostsMutation.ts | 2 +- .../mutation/useDeletePostGroupMutation.ts | 10 +- .../store/mutation/useDeletePostMutation.ts | 2 +- .../src/store/mutation/useLogoutMutation.ts | 2 +- .../useUpdateMultiplePromptMutation.ts | 10 +- .../useUpdatePersonalSettingMutation.ts | 2 +- .../store/mutation/useUpdatePostMutation.ts | 2 +- .../store/mutation/useUpdatePostsMutation.ts | 7 +- .../useUpdateReservedPostsMutation.ts | 2 +- .../useUpdateSinglePostPromptMutation.ts | 7 +- .../src/store/query/useGetAgentDetailQuery.ts | 2 +- .../store/query/useGetAgentPostGroupsQuery.ts | 2 +- apps/web/src/store/query/useGetAgentQuery.ts | 7 +- .../src/store/query/useGetAllPostsQuery.ts | 2 +- apps/web/src/store/query/useGetPostQuery.ts | 2 +- apps/web/src/store/query/useGetTopicQuery.ts | 2 +- apps/web/src/store/query/useGetUserQuery.ts | 2 +- apps/web/src/store/query/useGetXLogin.ts | 2 +- .../src/store/query/useNewsCategoriesQuery.ts | 17 +- .../src/store/query/usePostHistoryQuery.ts | 2 +- apps/web/src/types/post.ts | 1 + .../schedule => }/utils/getCurrentDateKo.ts | 0 apps/web/src/utils/index.ts | 1 + apps/web/src/utils/parseTime.ts | 16 +- packages/ui/src/components/Modal/Modal.css.ts | 4 +- packages/ui/src/components/Toast/Toast.css.ts | 1 + 113 files changed, 1863 insertions(+), 1205 deletions(-) create mode 100644 apps/web/src/app/(home)/[agentId]/_components/AgentDetailPersonalCard/AgentDetailPersonalCard.tsx create mode 100644 apps/web/src/app/(home)/[agentId]/_components/ContentGroupCard/ContentGroupCardSkeleton.tsx create mode 100644 apps/web/src/app/(home)/[agentId]/_components/PersonalCard/PersonalCardSkeleton.tsx create mode 100644 apps/web/src/app/(home)/[agentId]/_components/PostGroupsContentGroupCard/PostGroupsContentGroupCard.tsx create mode 100644 apps/web/src/app/(home)/[agentId]/_components/ReservedUploadContentCard/ReservedUploadContentCard.tsx create mode 100644 apps/web/src/app/(home)/[agentId]/_components/UploadContentCard/UploadContentCardSkeleton.tsx create mode 100644 apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/_components/EditContent/EditContentSkeleton.tsx create mode 100644 apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/_components/EditContent/EditContentWithDND.tsx create mode 100644 apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/_components/EditContent/index.ts create mode 100644 apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/detail/_providers/EditDetailPageProvider.tsx create mode 100644 apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/detail/type.ts delete mode 100644 apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/log/[postId]/Log.tsx delete mode 100644 apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/log/[postId]/_components/LogContentItem/LogContentItem.css.ts delete mode 100644 apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/log/[postId]/_components/LogContentItem/LogContentItem.tsx delete mode 100644 apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/log/[postId]/_components/LogSidebar/LogSidebar.css.ts delete mode 100644 apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/log/[postId]/_components/LogSidebar/LogSidebar.tsx delete mode 100644 apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/log/[postId]/page.tsx create mode 100644 apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/schedule/_components/ScheduleContent/ReadyToUploadPostsLength.tsx create mode 100644 apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/schedule/_components/ScheduleContent/ScheduleContent.tsx create mode 100644 apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/schedule/_components/ScheduleContent/ScheduleContentSkeleton.tsx create mode 100644 apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/schedule/_components/ScheduleContent/style.css.ts create mode 100644 apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/schedule/_components/SubmitBottomCTA/SubmitBottomCTA.tsx create mode 100644 apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/schedule/_components/SubmitBottomCTA/SubmitBottomCTASkeleton.tsx create mode 100644 apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/schedule/_components/SubmitBottomCTA/index.ts create mode 100644 apps/web/src/app/create/[agentId]/_components/NewsCategorySection/NewsCategorySection.tsx create mode 100644 apps/web/src/app/create/[agentId]/_components/NewsCategorySection/NewsCategorySectionSkeleton.tsx create mode 100644 apps/web/src/app/create/[agentId]/_components/NewsCategorySection/index.ts create mode 100644 apps/web/src/app/create/[agentId]/_components/NewsCategorySection/style.css.ts delete mode 100644 apps/web/src/app/loading.tsx create mode 100644 apps/web/src/app/personalize/[agentId]/_components/PersonalizeFormContent/PersonalizeFormContent.tsx create mode 100644 apps/web/src/app/personalize/[agentId]/_components/PersonalizeFormContent/PersonalizeFormContentSkeleton.tsx create mode 100644 apps/web/src/app/personalize/[agentId]/_components/PersonalizeFormContent/style.css.ts create mode 100644 apps/web/src/app/schedule/[agentId]/[postGroupId]/[postId]/_components/BreadcrumbContent/BreadcrumbContent.tsx create mode 100644 apps/web/src/app/schedule/[agentId]/[postGroupId]/[postId]/_components/BreadcrumbContent/BreadcrumbItemContentSkelton.tsx create mode 100644 apps/web/src/app/schedule/[agentId]/[postGroupId]/[postId]/_components/BreadcrumbContent/style.css.ts create mode 100644 apps/web/src/app/schedule/[agentId]/[postGroupId]/[postId]/_components/ScheduleDetailContent/ScheduleDetailContent.tsx create mode 100644 apps/web/src/app/schedule/[agentId]/[postGroupId]/[postId]/_components/ScheduleDetailContent/ScheduleDetailContentSkeleton.tsx create mode 100644 apps/web/src/app/schedule/[agentId]/[postGroupId]/[postId]/_components/ScheduleDetailContent/index.ts create mode 100644 apps/web/src/app/schedule/[agentId]/[postGroupId]/[postId]/_components/ScheduleDetailContent/style.css.ts create mode 100644 apps/web/src/app/schedule/[agentId]/_components/ScheduleContent/ScheduleContent.tsx create mode 100644 apps/web/src/app/schedule/[agentId]/_components/ScheduleContent/ScheduleContentSkeleton.tsx create mode 100644 apps/web/src/app/schedule/[agentId]/_components/ScheduleContent/style.css.ts create mode 100644 apps/web/src/components/common/AccountSidebar/AccountSidebarSkeleton.tsx create mode 100644 apps/web/src/components/common/BreadcrumbContent/BreadcrumbContent.tsx create mode 100644 apps/web/src/components/common/BreadcrumbContent/BreadcrumbItemContentSkelton.tsx create mode 100644 apps/web/src/components/common/BreadcrumbContent/style.css.ts create mode 100644 apps/web/src/components/common/UserProfileDropdown/UserProfileDropdown.css.ts create mode 100644 apps/web/src/components/common/UserProfileDropdown/UserProfileDropdown.tsx create mode 100644 apps/web/src/components/common/UserProfileDropdown/assets/iconNotice.png create mode 100644 apps/web/src/components/schedule/ScheduleTable/constants.ts rename apps/web/src/{app/(prompt)/edit/[agentId]/[postGroupId]/schedule => }/utils/getCurrentDateKo.ts (100%) diff --git a/apps/web/src/app/(home)/Home.tsx b/apps/web/src/app/(home)/Home.tsx index f6558218..b05ce584 100644 --- a/apps/web/src/app/(home)/Home.tsx +++ b/apps/web/src/app/(home)/Home.tsx @@ -1,6 +1,10 @@ 'use client'; -import { MainBreadcrumbItem, NavBar } from '@web/components/common'; +import { + MainBreadcrumbItem, + NavBar, + UserProfileDropdown, +} from '@web/components/common'; import { AccountSidebar } from '@web/components/common/AccountSidebar/AccountSidebar'; import { useScroll } from '@web/hooks'; import { ROUTES } from '@web/routes'; @@ -10,17 +14,10 @@ import { background, cardContent, content, - dropdownItem, - image, cardColumn, cardRow, flexColumn, } from './page.css'; -import { Dropdown } from '@repo/ui/Dropdown'; -import Image from 'next/image'; -import { Icon } from '@repo/ui/Icon'; -import { Text } from '@repo/ui/Text'; -import { isNil } from '@repo/ui/utils'; import { GradientAnimatedText } from '@repo/ui/GradientAnimatedText'; import CreateImage from '@web/assets/images/createImage.webp'; import { CTACard } from './[agentId]/_components/CTACard/CTACard'; @@ -28,50 +25,22 @@ import { PersonalCard } from './[agentId]/_components/PersonalCard/PersonalCard' import { UploadContentCard } from './[agentId]/_components/UploadContentCard/UploadContentCard'; import { ContentGroupCard } from './[agentId]/_components/ContentGroupCard/ContentGroupCard'; import { Spacing } from '@repo/ui/Spacing'; -import { getAgentQueryOptions } from '@web/store/query/useGetAgentQuery'; import { useRouter } from 'next/navigation'; import { Agent } from '@web/types'; -import { getUserQueryOptions } from '@web/store/query/useGetUserQuery'; -import { useLogoutMutation } from '@web/store/mutation/useLogoutMutation'; -import { useModal, useToast } from '@repo/ui/hooks'; -import { Modal } from '@repo/ui/Modal'; -import { useSuspenseQueries } from '@tanstack/react-query'; +import { useToast } from '@repo/ui/hooks'; export default function Home() { const router = useRouter(); const toast = useToast(); - const modal = useModal(); const [scrollRef, isScrolled] = useScroll({ threshold: 100, }); - const [{ data: user }, { data: agentData }] = useSuspenseQueries({ - queries: [getUserQueryOptions(), getAgentQueryOptions()], - }); - - const { mutate: logout } = useLogoutMutation(); - - const handleLogoutClick = () => { - modal.confirm({ - title: '정말 로그아웃 하시겠어요??', - icon: , - confirmButton: '로그아웃', - cancelButton: '취소', - confirmButtonProps: { - onClick: () => { - logout(); - }, - }, - }); - }; - const handleCreateClick = () => { toast.error('SNS 계정 연동이 필요해요.'); //TODO: 액션 필요 }; - const userData = user.data; - return (
} - rightAddon={ - - - {isNil(userData?.profileImage) ? ( -
- ) : ( - {''} - )} - - - - - - 로그아웃 - - - - - } + rightAddon={} isScrolled={isScrolled} />
router.push(ROUTES.HOME.DETAIL(id)) } @@ -129,7 +69,6 @@ export default function Home() {
- {/* 주제 생성 카드 */} - {/* 개인화 설정 카드 */} - +
- {/* 업로드 예약 일정 카드 */} - +
- - {/* 생성된 주제 카드 */} - +
diff --git a/apps/web/src/app/(home)/[agentId]/Home.tsx b/apps/web/src/app/(home)/[agentId]/Home.tsx index 1887a87c..12c04637 100644 --- a/apps/web/src/app/(home)/[agentId]/Home.tsx +++ b/apps/web/src/app/(home)/[agentId]/Home.tsx @@ -1,108 +1,42 @@ 'use client'; -import { MainBreadcrumbItem, NavBar } from '@web/components/common'; +import { + MainBreadcrumbItem, + NavBar, + UserProfileDropdown, +} from '@web/components/common'; import { AccountSidebar } from '../../../components/common/AccountSidebar/AccountSidebar'; import { useScroll } from '@web/hooks'; import { ROUTES } from '@web/routes'; -import { Breadcrumb } from '@repo/ui/Breadcrumb'; import { animatedText, background, cardContent, content, - dropdownItem, - image, cardColumn, cardRow, flexColumn, } from './page.css'; -import { Dropdown } from '@repo/ui/Dropdown'; -import Image from 'next/image'; -import { Icon } from '@repo/ui/Icon'; -import { Text } from '@repo/ui/Text'; -import { isNil } from '@repo/ui/utils'; -import { GradientAnimatedText } from '@repo/ui/GradientAnimatedText'; +import { Breadcrumb, GradientAnimatedText, Spacing } from '@repo/ui'; import CreateImage from '@web/assets/images/createImage.webp'; import { CTACard } from './_components/CTACard/CTACard'; -import { PersonalCard } from './_components/PersonalCard/PersonalCard'; -import { UploadContentCard } from './_components/UploadContentCard/UploadContentCard'; -import { ContentGroupCard } from './_components/ContentGroupCard/ContentGroupCard'; -import { Spacing } from '@repo/ui/Spacing'; -import { getAgentDetailQueryOptions } from '@web/store/query/useGetAgentDetailQuery'; -import { getAgentPostGroupsQueryOptions } from '@web/store/query/useGetAgentPostGroupsQuery'; -import { getAgentQueryOptions } from '@web/store/query/useGetAgentQuery'; -import { getAgentUploadReservedQueryOptions } from '@web/store/query/useGetAgentUploadReserved'; -import { getUserQueryOptions } from '@web/store/query/useGetUserQuery'; import { HomePageProps } from './types'; import { useRouter } from 'next/navigation'; -import { Agent, PostGroupId } from '@web/types'; -import { useModal } from '@repo/ui/hooks'; -import { Modal } from '@repo/ui/Modal'; -import { useDeletePostGroupMutation } from '@web/store/mutation/useDeletePostGroupMutation'; -import { useLogoutMutation } from '@web/store/mutation/useLogoutMutation'; -import { useSuspenseQueries } from '@tanstack/react-query'; +import { Agent } from '@web/types'; +import { AgentDetailPersonalCard } from './_components/AgentDetailPersonalCard/AgentDetailPersonalCard'; +import { Suspense } from 'react'; +import { PersonalCardSkeleton } from './_components/PersonalCard/PersonalCardSkeleton'; +import { ReservedUploadContentCard } from './_components/ReservedUploadContentCard/ReservedUploadContentCard'; +import { UploadContentCardSkeleton } from './_components/UploadContentCard/UploadContentCardSkeleton'; +import { PostGroupsContentGroupCard } from './_components/PostGroupsContentGroupCard/PostGroupsContentGroupCard'; +import { ContentGroupCardSkeleton } from './_components/ContentGroupCard/ContentGroupCardSkeleton'; export default function Home({ params }: HomePageProps) { const router = useRouter(); - const modal = useModal(); const [scrollRef, isScrolled] = useScroll({ threshold: 100, }); - const [ - { data: user }, - { data: agentDetail }, - { data: agentUploadReserved }, - { data: agentPostGroups }, - { data: agentData }, - ] = useSuspenseQueries({ - queries: [ - getUserQueryOptions(), - getAgentDetailQueryOptions({ agentId: params.agentId }), - getAgentUploadReservedQueryOptions({ agentId: params.agentId }), - getAgentPostGroupsQueryOptions({ agentId: params.agentId }), - getAgentQueryOptions(), - ], - }); - - const { mutate: deletePostGroups } = useDeletePostGroupMutation({ - agentId: params.agentId, - }); - const { mutate: logout } = useLogoutMutation(); - - const userData = user.data; - const agentDetailData = agentDetail.agentPersonalSetting; - const agentUploadReservedData = agentUploadReserved.posts.slice(0, 5); - - const handleDeletePostGroup = (postGroupId: PostGroupId) => { - modal.confirm({ - title: '정말 삭제하시겠어요?', - description: '삭제된 글은 복구할 수 없어요', - icon: , - confirmButton: '삭제하기', - cancelButton: '취소', - confirmButtonProps: { - onClick: () => { - deletePostGroups(postGroupId); - }, - }, - }); - }; - - const handleLogoutClick = () => { - modal.confirm({ - title: '정말 로그아웃 하시겠어요??', - icon: , - confirmButton: '로그아웃', - cancelButton: '취소', - confirmButtonProps: { - onClick: () => { - logout(); - }, - }, - }); - }; - return (
} - rightAddon={ - - - {isNil(userData?.profileImage) ? ( -
- ) : ( - {''} - )} - - - - - - 로그아웃 - - - - - } + rightAddon={} isScrolled={isScrolled} />
router.push(ROUTES.HOME.DETAIL(id)) } @@ -172,51 +77,30 @@ export default function Home({ params }: HomePageProps) { /> {/* 개인화 설정 카드 */} - - router.push(ROUTES.PERSONALIZE(params.agentId)) - } - /> + }> + + router.push(ROUTES.PERSONALIZE(params.agentId)) + } + /> +
{/* 업로드 예약 일정 카드 */} - - router.push(ROUTES.SCHEDULE.ROOT(params.agentId)) + } - onItemClick={(post) => { - router.push( - ROUTES.SCHEDULE.DETAIL({ - agentId: params.agentId, - postGroupId: post.postGroupId, - postId: post.id, - }) - ); - }} - items={agentUploadReservedData} - itemLength={agentUploadReserved.posts.length} - /> + > + +
{/* 생성된 주제 카드 */} - - router.push( - ROUTES.EDIT.ROOT({ - agentId: params.agentId, - postGroupId, - }) - ) - } - onItemRemove={(id) => { - handleDeletePostGroup(id); - }} - /> + }> + +
diff --git a/apps/web/src/app/(home)/[agentId]/_components/AgentDetailPersonalCard/AgentDetailPersonalCard.tsx b/apps/web/src/app/(home)/[agentId]/_components/AgentDetailPersonalCard/AgentDetailPersonalCard.tsx new file mode 100644 index 00000000..1315abf2 --- /dev/null +++ b/apps/web/src/app/(home)/[agentId]/_components/AgentDetailPersonalCard/AgentDetailPersonalCard.tsx @@ -0,0 +1,23 @@ +import { PersonalCard, PersonalCardProps } from '../PersonalCard/PersonalCard'; +import { useGetAgentDetailQuery } from '@web/store/query/useGetAgentDetailQuery'; +import { IdParams } from '@web/types'; + +type AgentDetailPersonalCardProps = Omit & { + agentId: IdParams['agentId']; +}; + +export function AgentDetailPersonalCard({ + onIconClick, + agentId, +}: AgentDetailPersonalCardProps) { + const { data: agentDetail } = useGetAgentDetailQuery({ + agentId: Number(agentId), + }); + + return ( + + ); +} diff --git a/apps/web/src/app/(home)/[agentId]/_components/ContentGroupCard/ContentGroupCard.css.ts b/apps/web/src/app/(home)/[agentId]/_components/ContentGroupCard/ContentGroupCard.css.ts index a673a003..ac20f5da 100644 --- a/apps/web/src/app/(home)/[agentId]/_components/ContentGroupCard/ContentGroupCard.css.ts +++ b/apps/web/src/app/(home)/[agentId]/_components/ContentGroupCard/ContentGroupCard.css.ts @@ -14,6 +14,7 @@ export const card = style({ export const leftText = style({ display: 'flex', + alignItems: 'center', gap: '0.8rem', }); diff --git a/apps/web/src/app/(home)/[agentId]/_components/ContentGroupCard/ContentGroupCard.tsx b/apps/web/src/app/(home)/[agentId]/_components/ContentGroupCard/ContentGroupCard.tsx index 2d84b374..8e74489c 100644 --- a/apps/web/src/app/(home)/[agentId]/_components/ContentGroupCard/ContentGroupCard.tsx +++ b/apps/web/src/app/(home)/[agentId]/_components/ContentGroupCard/ContentGroupCard.tsx @@ -26,14 +26,12 @@ import { Spacing } from '@repo/ui/Spacing'; import { isNotNil } from '@repo/ui/utils'; export type ContentGroupCardProps = { - text: string; postGroups?: PostGroup[]; onItemClick?: (PostGroupId: PostGroupId) => void; onItemRemove?: (PostGroupId: PostGroupId) => void; }; export function ContentGroupCard({ - text, postGroups, onItemClick, onItemRemove, @@ -42,7 +40,7 @@ export function ContentGroupCard({
- {text} + 생성된 주제 {postGroups?.length || 0} diff --git a/apps/web/src/app/(home)/[agentId]/_components/ContentGroupCard/ContentGroupCardSkeleton.tsx b/apps/web/src/app/(home)/[agentId]/_components/ContentGroupCard/ContentGroupCardSkeleton.tsx new file mode 100644 index 00000000..2d0184fe --- /dev/null +++ b/apps/web/src/app/(home)/[agentId]/_components/ContentGroupCard/ContentGroupCardSkeleton.tsx @@ -0,0 +1,35 @@ +import { Skeleton, Text } from '@repo/ui'; +import * as style from './ContentGroupCard.css'; +import { Spacing } from '@repo/ui'; + +export function ContentGroupCardSkeleton() { + return ( +
+
+ + 생성된 주제 + + +
+
+ {Array.from({ length: 9 }, (_, index) => ( +
+ +
+
+ + + + + +
+
+ +
+
+
+ ))} +
+
+ ); +} diff --git a/apps/web/src/app/(home)/[agentId]/_components/PersonalCard/PersonalCard.tsx b/apps/web/src/app/(home)/[agentId]/_components/PersonalCard/PersonalCard.tsx index 633a07a9..cb6f2cd8 100644 --- a/apps/web/src/app/(home)/[agentId]/_components/PersonalCard/PersonalCard.tsx +++ b/apps/web/src/app/(home)/[agentId]/_components/PersonalCard/PersonalCard.tsx @@ -15,13 +15,12 @@ import { Icon } from '@repo/ui/Icon'; import { isNotNil } from '@repo/ui/utils'; import { isEmptyStringOrNil } from '@web/utils'; -export type PersonalCardPops = { - text: string; +export type PersonalCardProps = { data?: AgentPersonalSetting; onIconClick?: () => void; }; -export function PersonalCard({ text, data, onIconClick }: PersonalCardPops) { +export function PersonalCard({ data, onIconClick }: PersonalCardProps) { return (
- {text} + 개인화 설정
{isNotNil(data) ? ( diff --git a/apps/web/src/app/(home)/[agentId]/_components/PersonalCard/PersonalCardSkeleton.tsx b/apps/web/src/app/(home)/[agentId]/_components/PersonalCard/PersonalCardSkeleton.tsx new file mode 100644 index 00000000..710b062d --- /dev/null +++ b/apps/web/src/app/(home)/[agentId]/_components/PersonalCard/PersonalCardSkeleton.tsx @@ -0,0 +1,27 @@ +import { Icon, Skeleton, Text } from '@repo/ui'; +import * as style from './PersonalCard.css'; + +export function PersonalCardSkeleton() { + return ( +
+
+
+ + 개인화 설정 + +
+ + +
+
+ +
+ +
+ ); +} diff --git a/apps/web/src/app/(home)/[agentId]/_components/PostGroupsContentGroupCard/PostGroupsContentGroupCard.tsx b/apps/web/src/app/(home)/[agentId]/_components/PostGroupsContentGroupCard/PostGroupsContentGroupCard.tsx new file mode 100644 index 00000000..6b845280 --- /dev/null +++ b/apps/web/src/app/(home)/[agentId]/_components/PostGroupsContentGroupCard/PostGroupsContentGroupCard.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import { ContentGroupCard } from '../ContentGroupCard/ContentGroupCard'; +import { useRouter } from 'next/navigation'; +import { ROUTES } from '@web/routes'; +import { IdParams, PostGroupId } from '@web/types'; +import { useModal } from '@repo/ui/hooks'; +import { Modal } from '@repo/ui'; +import { useDeletePostGroupMutation } from '@web/store/mutation/useDeletePostGroupMutation'; +import { useGetAgentPostGroupsQuery } from '@web/store/query/useGetAgentPostGroupsQuery'; + +type PostGroupsContentGroupCardProps = { + agentId: IdParams['agentId']; +}; + +export function PostGroupsContentGroupCard({ + agentId, +}: PostGroupsContentGroupCardProps) { + const router = useRouter(); + const modal = useModal(); + + const { data: agentPostGroups } = useGetAgentPostGroupsQuery({ + agentId: Number(agentId), + }); + + const { mutate: deletePostGroups } = useDeletePostGroupMutation({ + agentId: Number(agentId), + }); + + const handleDeletePostGroup = (postGroupId: PostGroupId) => { + modal.confirm({ + title: '정말 삭제하시겠어요?', + description: '삭제된 글은 복구할 수 없어요', + icon: , + confirmButton: '삭제하기', + cancelButton: '취소', + confirmButtonProps: { + onClick: () => { + deletePostGroups(postGroupId); + }, + }, + }); + }; + + return ( + + router.push( + ROUTES.EDIT.ROOT({ + agentId: Number(agentId), + postGroupId, + }) + ) + } + onItemRemove={(id) => { + handleDeletePostGroup(id); + }} + /> + ); +} diff --git a/apps/web/src/app/(home)/[agentId]/_components/ReservedUploadContentCard/ReservedUploadContentCard.tsx b/apps/web/src/app/(home)/[agentId]/_components/ReservedUploadContentCard/ReservedUploadContentCard.tsx new file mode 100644 index 00000000..46e88115 --- /dev/null +++ b/apps/web/src/app/(home)/[agentId]/_components/ReservedUploadContentCard/ReservedUploadContentCard.tsx @@ -0,0 +1,37 @@ +import { IdParams } from '@web/types'; +import { UploadContentCard } from '../UploadContentCard/UploadContentCard'; +import { useRouter } from 'next/navigation'; +import { ROUTES } from '@web/routes'; +import { useGetAgentUploadReservedQuery } from '@web/store/query/useGetAgentUploadReserved'; + +type ReservedUploadContentCardProps = { + agentId: IdParams['agentId']; +}; + +export function ReservedUploadContentCard({ + agentId, +}: ReservedUploadContentCardProps) { + const router = useRouter(); + const { data: agentUploadReserved } = useGetAgentUploadReservedQuery({ + agentId: Number(agentId), + }); + + const agentUploadReservedData = agentUploadReserved.posts.slice(0, 5); + + return ( + router.push(ROUTES.SCHEDULE.ROOT(agentId))} + onItemClick={(post) => { + router.push( + ROUTES.SCHEDULE.DETAIL({ + agentId: agentId, + postGroupId: post.postGroupId, + postId: post.id, + }) + ); + }} + items={agentUploadReservedData} + itemLength={agentUploadReserved.posts.length} + /> + ); +} diff --git a/apps/web/src/app/(home)/[agentId]/_components/UploadContentCard/UploadContentCard.css.ts b/apps/web/src/app/(home)/[agentId]/_components/UploadContentCard/UploadContentCard.css.ts index 16e44fc5..a3da5d70 100644 --- a/apps/web/src/app/(home)/[agentId]/_components/UploadContentCard/UploadContentCard.css.ts +++ b/apps/web/src/app/(home)/[agentId]/_components/UploadContentCard/UploadContentCard.css.ts @@ -64,3 +64,9 @@ export const emptyImage = style({ export const contentWrapper = style({ height: '100%', }); + +export const skeletonContentWrapper = style({ + display: 'flex', + flexDirection: 'column', + gap: '1.6rem', +}); diff --git a/apps/web/src/app/(home)/[agentId]/_components/UploadContentCard/UploadContentCard.tsx b/apps/web/src/app/(home)/[agentId]/_components/UploadContentCard/UploadContentCard.tsx index cafe0867..d458787e 100644 --- a/apps/web/src/app/(home)/[agentId]/_components/UploadContentCard/UploadContentCard.tsx +++ b/apps/web/src/app/(home)/[agentId]/_components/UploadContentCard/UploadContentCard.tsx @@ -18,7 +18,6 @@ import { Spacing } from '@repo/ui/Spacing'; import { isNotNil } from '@repo/ui/utils'; export type UploadContentCardProps = { - text: string; onMoreButtonClick?: () => void; items?: Post[]; onItemClick?: (post: Post) => void; @@ -26,7 +25,6 @@ export type UploadContentCardProps = { }; export function UploadContentCard({ - text, onMoreButtonClick, items, onItemClick, @@ -37,7 +35,7 @@ export function UploadContentCard({
- {text} + 업로드 예약 일정 {itemLength ?? 0} diff --git a/apps/web/src/app/(home)/[agentId]/_components/UploadContentCard/UploadContentCardSkeleton.tsx b/apps/web/src/app/(home)/[agentId]/_components/UploadContentCard/UploadContentCardSkeleton.tsx new file mode 100644 index 00000000..12a7b7cb --- /dev/null +++ b/apps/web/src/app/(home)/[agentId]/_components/UploadContentCard/UploadContentCardSkeleton.tsx @@ -0,0 +1,42 @@ +import { Button, Skeleton, Text } from '@repo/ui'; +import * as style from './UploadContentCard.css'; +import { useRouter } from 'next/navigation'; +import { IdParams } from '@web/types'; +import { ROUTES } from '@web/routes'; + +type UploadContentCardSkeletonProps = { + agentId: IdParams['agentId']; +}; + +export function UploadContentCardSkeleton({ + agentId, +}: UploadContentCardSkeletonProps) { + const router = useRouter(); + + return ( +
+
+
+ + 업로드 예약 일정 + + +
+ +
+
+ + + + + +
+
+ ); +} diff --git a/apps/web/src/app/(home)/[agentId]/page.tsx b/apps/web/src/app/(home)/[agentId]/page.tsx index 5ab1dd2e..c2b827b3 100644 --- a/apps/web/src/app/(home)/[agentId]/page.tsx +++ b/apps/web/src/app/(home)/[agentId]/page.tsx @@ -10,33 +10,29 @@ import { import { HomePageProps } from './types'; import { getUserQueryOptions } from '@web/store/query/useGetUserQuery'; import { getAgentUploadReservedQueryOptions } from '@web/store/query/useGetAgentUploadReserved'; -import { Suspense } from 'react'; -import Loading from '@web/app/loading'; export default function HomeDetailPage({ params }: HomePageProps) { const tokens = getServerSideTokens(); const serverFetchOptions = [ getAgentDetailQueryOptions({ - agentId: params.agentId, + agentId: Number(params.agentId), tokens, }), getAgentUploadReservedQueryOptions({ - agentId: params.agentId, + agentId: Number(params.agentId), tokens, }), getAgentPostGroupsQueryOptions({ - agentId: params.agentId, + agentId: Number(params.agentId), tokens, }), getAgentQueryOptions(tokens), getUserQueryOptions(tokens), - ]; + ] as FetchOptions[]; // TODO 임시 타입 단언 return ( - - }> - - + + ); } diff --git a/apps/web/src/app/(home)/page.tsx b/apps/web/src/app/(home)/page.tsx index 26198c2c..34ce5ce7 100644 --- a/apps/web/src/app/(home)/page.tsx +++ b/apps/web/src/app/(home)/page.tsx @@ -12,11 +12,10 @@ export default function HomePage() { const serverFetchOptions = [ getAgentQueryOptions(tokens), getUserQueryOptions(tokens), - ]; + ] as FetchOptions[]; // TODO 임시 타입 단언 return ( - // TODO 임시 타입 단언 - + ); diff --git a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/Edit.tsx b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/Edit.tsx index af48c898..8d06224e 100644 --- a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/Edit.tsx +++ b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/Edit.tsx @@ -2,32 +2,27 @@ import { useScroll } from '@web/hooks'; import * as style from './pageStyle.css'; -import { NavBar, MainBreadcrumbItem } from '@web/components/common'; -import { Breadcrumb, FixedBottomCTA, Icon } from '@repo/ui'; -import { POST_STATUS } from '@web/types/post'; -import { DndController } from '@web/components/common'; +import { + NavBar, + MainBreadcrumbItem, + BreadcrumbItemContentSkelton, + BreadcrumbItemContent, +} from '@web/components/common'; +import { Breadcrumb } from '@repo/ui'; import { EditPageProps } from './types'; -import { useGetAllPostsQuery } from '@web/store/query/useGetAllPostsQuery'; -import { useUpdatePostsMutation } from '@web/store/mutation/useUpdatePostsMutation'; -import { useRouter } from 'next/navigation'; -import { EditContent } from './_components/EditContent/EditContent'; -import { ContentItem } from '@web/components/common/DNDController/compounds'; import { ROUTES } from '@web/routes'; +import { Suspense } from 'react'; +import { + EditContentWithDND, + EditContentSkeleton, +} from './_components/EditContent'; +import { + SubmitBottomCTA, + SubmitBottomCTASkeleton, +} from './schedule/_components/SubmitBottomCTA'; export default function Edit({ params }: EditPageProps) { const [scrollRef, isScrolled] = useScroll({ threshold: 100 }); - const { data: posts } = useGetAllPostsQuery({ - agentId: params.agentId, - postGroupId: params.postGroupId, - }); - const { mutate: updatePosts } = useUpdatePostsMutation({ - agentId: params.agentId, - postGroupId: params.postGroupId, - }); - const router = useRouter(); - - const hasReadyToUploadPosts = - posts.data.posts[POST_STATUS.READY_TO_UPLOAD].length > 0; return ( <> @@ -36,58 +31,31 @@ export default function Edit({ params }: EditPageProps) { leftAddon={ - - {posts.data.postGroup.topic} - + }> + + } isScrolled={isScrolled} /> - - `${item.id}-${item.displayOrder}-${item.status}`) - .join(',')} - onDragEnd={(updatedItems) => { - const updatePayload = { - posts: Object.values(updatedItems) - .flat() - .map((item) => ({ - postId: item.id, - status: item.status, - displayOrder: item.displayOrder, - uploadTime: item.uploadTime, - })), - }; - updatePosts(updatePayload); - }} - renderDragOverlay={(activeItem) => ( - - )} - > - - + }> + +
- } - onClick={() => - router.push( - ROUTES.EDIT.SCHEDULE({ - agentId: params.agentId, - postGroupId: params.postGroupId, - }) - ) - } - disabled={!hasReadyToUploadPosts} - > - 예약하러 가기 - + }> + + 예약하러 가기 + + ); } diff --git a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/_components/EditContent/EditContentSkeleton.tsx b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/_components/EditContent/EditContentSkeleton.tsx new file mode 100644 index 00000000..4e4d078a --- /dev/null +++ b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/_components/EditContent/EditContentSkeleton.tsx @@ -0,0 +1,84 @@ +'use client'; + +import { Chip, Accordion } from '@repo/ui'; +import { POST_STATUS } from '@web/types/post'; +import * as style from './EditContent.css'; +import { SkeletonContentItem } from '../SkeletonContentItem/SkeletonContentItem'; + +export function EditContentSkeleton() { + return ( +
+ + {/* 생성된 글 영역 */} + + + + } + > + 생성된 글 + + + +
+ +
+
+
+ + {/* 수정 중인 글 영역 */} + + + + } + > + 수정 중인 글 + + + + + + + + {/* 업로드할 글 영역 */} + + + + } + > + 업로드할 글 + + + + + + +
+
+ ); +} diff --git a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/_components/EditContent/EditContentWithDND.tsx b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/_components/EditContent/EditContentWithDND.tsx new file mode 100644 index 00000000..e92bbb90 --- /dev/null +++ b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/_components/EditContent/EditContentWithDND.tsx @@ -0,0 +1,57 @@ +import { DndController } from '@web/components/common'; +import { useUpdatePostsMutation } from '@web/store/mutation/useUpdatePostsMutation'; +import { useGetAllPostsQuery } from '@web/store/query/useGetAllPostsQuery'; +import { IdParams } from '@web/types'; +import React from 'react'; +import { ContentItem } from '../ContentItem/ContentItem'; +import { EditContent } from './EditContent'; + +type EditContentWithDNDProps = Omit; + +export function EditContentWithDND({ + agentId, + postGroupId, +}: EditContentWithDNDProps) { + const { data: posts } = useGetAllPostsQuery({ + agentId: Number(agentId), + postGroupId: Number(postGroupId), + }); + + const { mutate: updatePosts } = useUpdatePostsMutation({ + agentId: Number(agentId), + postGroupId: Number(postGroupId), + }); + + return ( + `${item.id}-${item.displayOrder}-${item.status}`) + .join(',')} + onDragEnd={(updatedItems) => { + const updatePayload = { + posts: Object.values(updatedItems) + .flat() + .map((item) => ({ + postId: item.id, + status: item.status, + displayOrder: item.displayOrder, + uploadTime: item.uploadTime, + })), + }; + updatePosts(updatePayload); + }} + renderDragOverlay={(activeItem) => ( + + )} + > + + + ); +} diff --git a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/_components/EditContent/index.ts b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/_components/EditContent/index.ts new file mode 100644 index 00000000..ea4c8b4f --- /dev/null +++ b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/_components/EditContent/index.ts @@ -0,0 +1,2 @@ +export { EditContentSkeleton } from './EditContentSkeleton'; +export { EditContentWithDND } from './EditContentWithDND'; diff --git a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/detail/EditDetail.tsx b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/detail/EditDetail.tsx index 6fe9444e..f8e72516 100644 --- a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/detail/EditDetail.tsx +++ b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/detail/EditDetail.tsx @@ -1,31 +1,14 @@ 'use client'; -import { createContext, Dispatch, SetStateAction } from 'react'; import { EditPost } from './_components/EditPost/EditPost'; import { EditSidebar } from './_components/EditSidebar/EditSidebar'; import { editDetailPage, flexColumn } from './page.css'; -import { useState } from 'react'; -import { Post } from '@web/types'; import { Suspense } from 'react'; -// TODO 추후 Jotai, 또는 react-query 사용으로 수정할 예정 -interface DetailPageContextType { - loadingPosts: Post['id'][]; - setLoadingPosts: Dispatch>; -} - -const defaultContextValue: DetailPageContextType = { - loadingPosts: [], - setLoadingPosts: () => {}, -}; - -export const DetailPageContext = - createContext(defaultContextValue); +import { EditDetailPageProvider } from './_providers/EditDetailPageProvider'; export function EditDetail() { - const [loadingPosts, setLoadingPosts] = useState([]); - return ( - +
@@ -36,6 +19,6 @@ export function EditDetail() {
- + ); } diff --git a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/detail/_components/EditPost/EditPost.tsx b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/detail/_components/EditPost/EditPost.tsx index c7c4cb90..89c7688e 100644 --- a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/detail/_components/EditPost/EditPost.tsx +++ b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/detail/_components/EditPost/EditPost.tsx @@ -10,7 +10,6 @@ import { wrapper, } from './EditPost.css'; import { Text } from '@repo/ui/Text'; -import { Badge } from '@repo/ui/Badge'; import { PostEditor } from '../PostEditor/PostEditor'; import { EditPromptField } from '../EditPromptField/EditPromptField'; import { FormProvider, useForm } from 'react-hook-form'; @@ -27,7 +26,7 @@ import { Chip } from '@repo/ui/Chip'; import { PostStatus } from '@web/types'; import { ReactNode, useContext } from 'react'; import { useUpdatePostsMutation } from '@web/store/mutation/useUpdatePostsMutation'; -import { DetailPageContext } from '../../EditDetail'; +import { EditDetailPageContext } from '../../_providers/EditDetailPageProvider'; import { Skeleton } from '@repo/ui'; import { SkeletonEditor } from '../PostEditor/SkeletonEditor'; @@ -65,7 +64,7 @@ export function EditPost() { const { agentId, postGroupId } = useParams(); const searchParams = useSearchParams(); const postId = Number(searchParams.get('postId')); - const { loadingPosts } = useContext(DetailPageContext); + const { loadingPosts } = useContext(EditDetailPageContext); const isPostLoading = loadingPosts.includes(postId); const { data: posts } = useGetAllPostsQuery({ agentId: Number(agentId), diff --git a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/detail/_components/EditPromptField/EditPromptField.tsx b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/detail/_components/EditPromptField/EditPromptField.tsx index 28e85ced..60e921c3 100644 --- a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/detail/_components/EditPromptField/EditPromptField.tsx +++ b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/detail/_components/EditPromptField/EditPromptField.tsx @@ -5,7 +5,7 @@ import { wrapper } from './EditPromptField.css'; import { isEmptyStringOrNil } from '@web/utils'; import { useParams, useSearchParams } from 'next/navigation'; import { useContext, useEffect } from 'react'; -import { DetailPageContext } from '../../EditDetail'; +import { EditDetailPageContext } from '../../_providers/EditDetailPageProvider'; import { useUpdateSinglePostPromptMutation } from '@web/store/mutation/useUpdateSinglePostPromptMutation'; export function EditPromptField() { @@ -16,7 +16,7 @@ export function EditPromptField() { prompt: '', }, }); - const { loadingPosts, setLoadingPosts } = useContext(DetailPageContext); + const { loadingPosts, setLoadingPosts } = useContext(EditDetailPageContext); const prompt = watch('prompt'); const isSubmitDisabled = isEmptyStringOrNil(prompt); const { agentId, postGroupId } = useParams(); diff --git a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/detail/_components/EditSidebar/EditSidebar.tsx b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/detail/_components/EditSidebar/EditSidebar.tsx index 169b3b0e..83fb5f31 100644 --- a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/detail/_components/EditSidebar/EditSidebar.tsx +++ b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/detail/_components/EditSidebar/EditSidebar.tsx @@ -26,7 +26,7 @@ import { useCreateMorePostsMutation } from '@web/store/mutation/useCreateMorePos import { useModal } from '@repo/ui/hooks'; import { Modal } from '@repo/ui/Modal'; import { useDeletePostMutation } from '@web/store/mutation/useDeletePostMutation'; -import { DetailPageContext } from '../../EditDetail'; +import { EditDetailPageContext } from '../../_providers/EditDetailPageProvider'; import { DragGuide } from '../DragGuide/DragGuide'; import { ContentItem } from '@web/components/common/DNDController/compounds'; import { ROUTES } from '@web/routes'; @@ -43,7 +43,7 @@ import { isEmptyStringOrNil } from '@web/utils'; function EditSidebarContent() { const modal = useModal(); - const { loadingPosts, setLoadingPosts } = useContext(DetailPageContext); + const { loadingPosts, setLoadingPosts } = useContext(EditDetailPageContext); const { agentId, postGroupId } = useParams(); const router = useRouter(); const searchParams = useSearchParams(); diff --git a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/detail/_components/PostEditor/PostEditor.tsx b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/detail/_components/PostEditor/PostEditor.tsx index 42dfad67..993cf66c 100644 --- a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/detail/_components/PostEditor/PostEditor.tsx +++ b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/detail/_components/PostEditor/PostEditor.tsx @@ -15,7 +15,7 @@ import { Text } from '@repo/ui/Text'; import { Button } from '@repo/ui/Button'; import EmojiPicker from 'emoji-picker-react'; import { useForm } from 'react-hook-form'; -import { useContext, useEffect, useRef, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { isNotNil, mergeRefs } from '@repo/ui/utils'; import { UploadedImages } from './UploadedImages'; import { useParams, useSearchParams } from 'next/navigation'; @@ -26,7 +26,6 @@ import { useUpdatePostMutation } from '@web/store/mutation/useUpdatePostMutation import { Post, PostGroupLength } from '@web/types'; import { useGetAllPostsQuery } from '@web/store/query/useGetAllPostsQuery'; import { useToast } from '@repo/ui/hooks'; -import { DetailPageContext } from '../../EditDetail'; const POST_LENGTH: Record = { LONG: 1000, @@ -39,8 +38,6 @@ export function PostEditor() { const { agentId, postGroupId } = useParams(); const searchParams = useSearchParams(); const postId = Number(searchParams.get('postId')); - const { loadingPosts } = useContext(DetailPageContext); - const isPostLoading = loadingPosts.includes(postId); const { data: posts } = useGetAllPostsQuery({ agentId: Number(agentId), postGroupId: Number(postGroupId), diff --git a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/detail/_hooks/useAdjacentPosts.tsx b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/detail/_hooks/useAdjacentPosts.tsx index d9d0c82f..25c8d862 100644 --- a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/detail/_hooks/useAdjacentPosts.tsx +++ b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/detail/_hooks/useAdjacentPosts.tsx @@ -11,8 +11,9 @@ export function useAdjacentPosts(posts: PostsByStatus, currentPost?: Post) { EDITING: 2, READY_TO_UPLOAD: 1, UPLOAD_RESERVED: 0, - UPLOADED: -1, - UPLOAD_FAILED: -2, + UPLOAD_CONFIRMED: -1, + UPLOADED: -2, + UPLOAD_FAILED: -3, }; const allPosts = useMemo(() => { diff --git a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/detail/_providers/EditDetailPageProvider.tsx b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/detail/_providers/EditDetailPageProvider.tsx new file mode 100644 index 00000000..35621992 --- /dev/null +++ b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/detail/_providers/EditDetailPageProvider.tsx @@ -0,0 +1,37 @@ +import { + createContext, + Dispatch, + ReactNode, + SetStateAction, + useState, +} from 'react'; +import { Post } from '@web/types'; + +interface EditDetailPageContextType { + loadingPosts: Post['id'][]; + setLoadingPosts: Dispatch>; +} + +const defaultContextValue: EditDetailPageContextType = { + loadingPosts: [], + setLoadingPosts: () => {}, +}; + +export const EditDetailPageContext = + createContext(defaultContextValue); + +interface EditDetailPageProviderProps { + children: ReactNode; +} + +export function EditDetailPageProvider({ + children, +}: EditDetailPageProviderProps) { + const [loadingPosts, setLoadingPosts] = useState([]); + + return ( + + {children} + + ); +} diff --git a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/detail/page.tsx b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/detail/page.tsx index 20f699c8..9c75d464 100644 --- a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/detail/page.tsx +++ b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/detail/page.tsx @@ -1,5 +1,25 @@ import { EditDetail } from './EditDetail'; +import { getServerSideTokens } from '@web/shared/server/serverSideTokens'; +import { + FetchOptions, + ServerFetchBoundary, +} from '@web/store/query/ServerFetchBoundary'; +import { getAllPostsQueryOptions } from '@web/store/query/useGetAllPostsQuery'; +import { EditDetailPageProps } from './type'; -export default function EditDetailPage() { - return ; +export default function EditDetailPage({ params }: EditDetailPageProps) { + const tokens = getServerSideTokens(); + const serverFetchOptions = [ + getAllPostsQueryOptions({ + agentId: Number(params.agentId), + postGroupId: Number(params.postGroupId), + tokens, + }), + ] as FetchOptions[]; + + return ( + + + + ); } diff --git a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/detail/type.ts b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/detail/type.ts new file mode 100644 index 00000000..e1820755 --- /dev/null +++ b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/detail/type.ts @@ -0,0 +1,5 @@ +import { IdParams } from '@web/types'; + +export type EditDetailPageProps = { + params: Omit; +}; diff --git a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/log/[postId]/Log.tsx b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/log/[postId]/Log.tsx deleted file mode 100644 index a215673b..00000000 --- a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/log/[postId]/Log.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export function Log() { - return
dd
; -} diff --git a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/log/[postId]/_components/LogContentItem/LogContentItem.css.ts b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/log/[postId]/_components/LogContentItem/LogContentItem.css.ts deleted file mode 100644 index 72b8849e..00000000 --- a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/log/[postId]/_components/LogContentItem/LogContentItem.css.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { vars } from '@repo/theme'; -import { style } from '@vanilla-extract/css'; - -export const wrapper = style({ - width: '100%', - height: '13.4rem', - padding: '1.2rem', - display: 'flex', - flexDirection: 'column', - gap: '0.8rem', - borderRadius: `${vars.borderRadius[10]}`, - ':hover': { - backgroundColor: `${vars.colors.grey25}`, - }, -}); - -export const promptText = style({ - width: '100%', - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - marginRight: '0.8rem', -}); diff --git a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/log/[postId]/_components/LogContentItem/LogContentItem.tsx b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/log/[postId]/_components/LogContentItem/LogContentItem.tsx deleted file mode 100644 index 44b340bd..00000000 --- a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/log/[postId]/_components/LogContentItem/LogContentItem.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { Badge } from '@repo/ui/Badge'; -import { promptText, wrapper } from './LogContentItem.css'; -import { Text } from '@repo/ui/Text'; -import { getTimeAgo } from '@web/utils'; - -export type LogContentItemProps = { - type: 'EACH' | 'ALL'; - createdAt: Date | string; - id: number; - prompt: string; - response: string; -}; -type BadgeInfo = { - color: 'pink' | 'blue'; - text: string; -}; - -const BadgeVariants: Record = { - ALL: { - color: 'pink', - text: '개별 적용', - }, - EACH: { - color: 'blue', - text: '일괄 적용', - }, -}; -export function LogContentItem({ - type, - createdAt, - id, - prompt, - response, -}: LogContentItemProps) { - const { color, text } = BadgeVariants[type]; - return ( -
- - {text} - - - {prompt} - - - {/* TODO 디자인 시안과 동일하게 */} - {getTimeAgo(createdAt)} - -
- ); -} diff --git a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/log/[postId]/_components/LogSidebar/LogSidebar.css.ts b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/log/[postId]/_components/LogSidebar/LogSidebar.css.ts deleted file mode 100644 index 7c807f1f..00000000 --- a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/log/[postId]/_components/LogSidebar/LogSidebar.css.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { vars } from '@repo/theme'; -import { style } from '@vanilla-extract/css'; - -export const sidebarWrapper = style({ - width: '42.3rem', - height: '100vh', - flexShrink: '0', - backgroundColor: `${vars.colors.grey}`, -}); - -export const closeArea = style({ - padding: '2rem 2.4rem', - display: 'flex', - justifySelf: 'flex-end', -}); diff --git a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/log/[postId]/_components/LogSidebar/LogSidebar.tsx b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/log/[postId]/_components/LogSidebar/LogSidebar.tsx deleted file mode 100644 index 14ceec53..00000000 --- a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/log/[postId]/_components/LogSidebar/LogSidebar.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { IconButton } from '@repo/ui/IconButton'; -import { closeArea, sidebarWrapper } from './LogSidebar.css'; -import { useParams, useRouter, useSearchParams } from 'next/navigation'; -import { ROUTES } from '@web/routes'; - -export function LogSidebar() { - const router = useRouter(); - const { agentId, postGroupId } = useParams(); - const searchParams = useSearchParams(); - const postId = searchParams.get('postId'); - const handleXClick = () => { - router.push( - ROUTES.EDIT.DETAIL({ - agentId: Number(agentId), - postGroupId: Number(postGroupId), - postId: Number(postId), - }) - ); - }; - return ( -
-
- -
-
- ); -} diff --git a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/log/[postId]/page.tsx b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/log/[postId]/page.tsx deleted file mode 100644 index ea501d31..00000000 --- a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/log/[postId]/page.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { ServerFetchBoundary } from '@web/store/query/ServerFetchBoundary'; - -import { getServerSideTokens } from '@web/shared/server/serverSideTokens'; - -import { Log } from './Log'; -import { PostHistoryQueryQueryOptions } from '@web/store/query/usePostHistoryQuery'; - -type LogPageProps = { - params: { agentId: string; postGroupId: string; postId: string }; -}; - -export default function LogPage({ params }: LogPageProps) { - const tokens = getServerSideTokens(); - - const serverFetchOptions = PostHistoryQueryQueryOptions({ - agentId: Number(params.agentId), - postGroupId: Number(params.postGroupId), - postId: Number(params.postId), - tokens, - }); - return ( - - - - ); -} diff --git a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/page.tsx b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/page.tsx index b8a5255e..259902b8 100644 --- a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/page.tsx +++ b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/page.tsx @@ -3,22 +3,18 @@ import Edit from './Edit'; import type { EditPageProps } from './types'; import { getAllPostsQueryOptions } from '@web/store/query/useGetAllPostsQuery'; import { getServerSideTokens } from '@web/shared/server/serverSideTokens'; -import { Suspense } from 'react'; -import Loading from '@web/app/loading'; export default function EditPage({ params }: EditPageProps) { const tokens = getServerSideTokens(); const serverFetchOptions = getAllPostsQueryOptions({ - agentId: params.agentId, - postGroupId: params.postGroupId, + agentId: Number(params.agentId), + postGroupId: Number(params.postGroupId), tokens, }); return ( - }> - - + ); } diff --git a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/schedule/Schedule.tsx b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/schedule/Schedule.tsx index 8cdfa756..9c3fc762 100644 --- a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/schedule/Schedule.tsx +++ b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/schedule/Schedule.tsx @@ -2,42 +2,41 @@ import { useScroll } from '@web/hooks'; import * as style from './pageStyle.css'; -import { NavBar, MainBreadcrumbItem } from '@web/components/common'; +import { + NavBar, + MainBreadcrumbItem, + BreadcrumbItemContentSkelton, +} from '@web/components/common'; import { Breadcrumb, Button, FixedBottomCTA, Icon } from '@repo/ui'; -import { DndController } from '@web/components/common'; -import { useGetAllPostsQuery } from '@web/store/query/useGetAllPostsQuery'; -import { useUpdatePostsMutation } from '@web/store/mutation/useUpdatePostsMutation'; -import { TitleWithDescription } from '@web/components/common/TitleWithDescription/TitleWithDescription'; import { useRouter } from 'next/navigation'; -import { ScheduleTable } from '@web/components/schedule/ScheduleTable/ScheduleTable'; -import { EditPageProps } from '../types'; import { ROUTES } from '@web/routes'; -import { POST_STATUS } from '@web/types'; -import { ContentItem } from '../_components/ContentItem/ContentItem'; import { useForm, FormProvider } from 'react-hook-form'; import { validateScheduleDate } from '@web/utils/validateScheduleDate'; import { useToast } from '@repo/ui/hooks'; import { isNotNil } from '@repo/ui/utils'; -import { getCurrentDateKo } from './utils/getCurrentDateKo'; +import { getFormattedHourByAMPM } from '@web/utils'; +import { Suspense } from 'react'; +import { ScheduleContent } from './_components/ScheduleContent/ScheduleContent'; +import { ScheduleContentSkeleton } from './_components/ScheduleContent/ScheduleContentSkeleton'; +import { useUpdatePostsMutation } from '@web/store/mutation/useUpdatePostsMutation'; +import { POST_STATUS } from '@web/types'; +import { EditPageProps, ScheduleFormValues } from '../types'; +import { BreadcrumbItemContent } from '@web/components/common'; +import { useQueryClient } from '@tanstack/react-query'; +import { getAgentUploadReservedQueryOptions } from '@web/store/query/useGetAgentUploadReserved'; export default function Schedule({ params }: EditPageProps) { const [scrollRef, isScrolled] = useScroll({ threshold: 100, }); - const { data: posts } = useGetAllPostsQuery(params); - const { mutate: updatePosts } = useUpdatePostsMutation(params); - const readyToUploadPosts = posts.data.posts.READY_TO_UPLOAD; const router = useRouter(); const toast = useToast(); + const { mutate: updatePosts } = useUpdatePostsMutation(params); + const queryClient = useQueryClient(); - const methods = useForm({ + const methods = useForm({ defaultValues: { - schedules: readyToUploadPosts.map((post) => ({ - postId: post.id, - date: getCurrentDateKo(), - hour: '00', - minute: '00', - })), + schedules: [], }, }); @@ -46,11 +45,16 @@ export default function Schedule({ params }: EditPageProps) { if ( isNotNil(schedule.date) && isNotNil(schedule.hour) && - isNotNil(schedule.minute) + isNotNil(schedule.minute) && + isNotNil(schedule.amPm) ) { + const formattedHour = getFormattedHourByAMPM( + schedule.hour, + schedule.amPm + ); return validateScheduleDate( schedule.date, - schedule.hour, + formattedHour, schedule.minute ); } @@ -62,34 +66,50 @@ export default function Schedule({ params }: EditPageProps) { return; } - const updatePayload = { - posts: readyToUploadPosts.map((post, index) => ({ - postId: post.id, - status: POST_STATUS.UPLOAD_RESERVED, - displayOrder: post.displayOrder, - uploadTime: `${data.schedules[index]?.date}T${data.schedules[index]?.hour}:${data.schedules[index]?.minute}:00`, // TODO: 임시 구현. 추후 타입 가드 필요 - })), - }; - - updatePosts(updatePayload, { - onSuccess: () => { - toast.success('예약이 완료되었어요'); - router.push(ROUTES.HOME.DETAIL(params.agentId)); + updatePosts( + { + posts: data.schedules.map((schedule) => ({ + postId: schedule.postId, + status: POST_STATUS.UPLOAD_CONFIRMED, + uploadTime: `${schedule.date}T${getFormattedHourByAMPM( + schedule.hour, + schedule.amPm + )}:${schedule.minute}:00`, + })), }, - }); + { + onSuccess: () => { + queryClient.invalidateQueries( + getAgentUploadReservedQueryOptions({ + agentId: Number(params.agentId), + }) + ); + toast.success('예약이 완료되었어요'); + router.push(ROUTES.HOME.DETAIL(params.agentId)); + }, + } + ); }); return ( <> -
+ - - {posts.data.postGroup.topic} - + }> + + } rightAddon={ @@ -101,8 +121,8 @@ export default function Schedule({ params }: EditPageProps) { onClick={() => router.push( ROUTES.EDIT.ROOT({ - agentId: Number(params.agentId), - postGroupId: Number(params.postGroupId), + agentId: params.agentId, + postGroupId: params.postGroupId, }) ) } @@ -114,34 +134,20 @@ export default function Schedule({ params }: EditPageProps) { isScrolled={isScrolled} />
-
- }> + - `${item.id}-${item.displayOrder}-${item.status}` - ) - .join(',')} - renderDragOverlay={(activeItem) => ( - - )} - > - - -
+
- }> + } + > 예약 완료 diff --git a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/schedule/_components/ScheduleContent/ReadyToUploadPostsLength.tsx b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/schedule/_components/ScheduleContent/ReadyToUploadPostsLength.tsx new file mode 100644 index 00000000..c50e6d59 --- /dev/null +++ b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/schedule/_components/ScheduleContent/ReadyToUploadPostsLength.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export function ReadyToUploadPostsLength() { + return
ReadyToUploadPostsLength
; +} diff --git a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/schedule/_components/ScheduleContent/ScheduleContent.tsx b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/schedule/_components/ScheduleContent/ScheduleContent.tsx new file mode 100644 index 00000000..1d7cd7f7 --- /dev/null +++ b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/schedule/_components/ScheduleContent/ScheduleContent.tsx @@ -0,0 +1,69 @@ +'use client'; + +import { useGetAllPostsQuery } from '@web/store/query/useGetAllPostsQuery'; +import { ScheduleTable } from '@web/components/schedule/ScheduleTable/ScheduleTable'; +import { DndController } from '@web/components/common'; +import { POST_STATUS } from '@web/types'; +import { TitleWithDescription } from '@web/components/common/TitleWithDescription/TitleWithDescription'; +import * as style from './style.css'; +import { ContentItem } from '../../../_components/ContentItem/ContentItem'; +import { IdParams } from '@web/types'; +import { useFormContext } from 'react-hook-form'; +import { useEffect } from 'react'; +import { getCurrentDateKo } from '@web/utils'; +import { ScheduleFormValues } from '../../../types'; + +type ScheduleContentProps = Omit; + +export function ScheduleContent({ + agentId, + postGroupId, +}: ScheduleContentProps) { + const { data: posts } = useGetAllPostsQuery({ + agentId: Number(agentId), + postGroupId: Number(postGroupId), + }); + const readyToUploadPosts = posts.data.posts.READY_TO_UPLOAD; + const { setValue } = useFormContext(); + + useEffect(() => { + const currentDate = getCurrentDateKo() ?? ''; + if (!readyToUploadPosts) { + return; + } + + setValue( + 'schedules', + readyToUploadPosts.map((post) => ({ + postId: post.id, + date: currentDate, + hour: '12', + minute: '00', + amPm: '오전', + })) + ); + }, [readyToUploadPosts, setValue]); + + return ( +
+ + `${item.id}-${item.displayOrder}-${item.status}`) + .join(',')} + renderDragOverlay={(activeItem) => } + > + + +
+ ); +} diff --git a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/schedule/_components/ScheduleContent/ScheduleContentSkeleton.tsx b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/schedule/_components/ScheduleContent/ScheduleContentSkeleton.tsx new file mode 100644 index 00000000..da86ee7d --- /dev/null +++ b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/schedule/_components/ScheduleContent/ScheduleContentSkeleton.tsx @@ -0,0 +1,28 @@ +import { Skeleton, Spacing } from '@repo/ui'; +import * as style from './style.css'; +import { TitleWithDescription } from '@web/components/common/TitleWithDescription/TitleWithDescription'; +import { columns } from '@web/components/schedule/ScheduleTable/constants'; +import { Table } from '@web/components/common'; + +export function ScheduleContentSkeleton() { + const totalWidth = columns.reduce((acc, column) => { + const width = parseFloat(column.width); + return acc + width; + }, 0); + + return ( +
+ } + description="개별 글의 업로드 날짜와 순서를 변경할 수 있어요" + /> +
+ }> + + +
+
+
+ ); +} diff --git a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/schedule/_components/ScheduleContent/style.css.ts b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/schedule/_components/ScheduleContent/style.css.ts new file mode 100644 index 00000000..12dee26f --- /dev/null +++ b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/schedule/_components/ScheduleContent/style.css.ts @@ -0,0 +1,36 @@ +import { vars } from '@repo/theme'; +import { style } from '@vanilla-extract/css'; + +export const skeletonWrapperStyle = style({ + position: 'relative', + display: 'flex', + flexDirection: 'column', + width: '100%', + justifyContent: 'center', + alignItems: 'center', + gap: vars.space[16], +}); + +export const dndSectionStyle = style({ + width: '100rem', + position: 'relative', + margin: '0 auto', +}); + +export const textWrapperStyle = style({ + display: 'flex', + flexDirection: 'column', + gap: vars.space[8], + padding: `${vars.space[24]} 0`, +}); + +export const titleWrapperStyle = style({ + display: 'flex', + flexDirection: 'row', + gap: vars.space[8], +}); + +export const tableContainer = style({ + position: 'relative', + width: '100%', +}); diff --git a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/schedule/_components/SubmitBottomCTA/SubmitBottomCTA.tsx b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/schedule/_components/SubmitBottomCTA/SubmitBottomCTA.tsx new file mode 100644 index 00000000..df785ccd --- /dev/null +++ b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/schedule/_components/SubmitBottomCTA/SubmitBottomCTA.tsx @@ -0,0 +1,43 @@ +import { FixedBottomCTA, FixedBottomCTAProps, Icon } from '@repo/ui'; +import { ROUTES } from '@web/routes'; +import { useGetAllPostsQuery } from '@web/store/query/useGetAllPostsQuery'; +import { IdParams, POST_STATUS } from '@web/types'; +import { useRouter } from 'next/navigation'; + +type SubmitBottomCTAProps = Omit & FixedBottomCTAProps; + +export function SubmitBottomCTA({ + agentId, + postGroupId, + children, + ...props +}: SubmitBottomCTAProps) { + const { data: posts } = useGetAllPostsQuery({ + agentId: Number(agentId), + postGroupId: Number(postGroupId), + }); + + const hasReadyToUploadPosts = + posts.data.posts[POST_STATUS.READY_TO_UPLOAD].length > 0; + + const router = useRouter(); + + return ( + } + onClick={() => + router.push( + ROUTES.EDIT.SCHEDULE({ + agentId: agentId, + postGroupId: postGroupId, + }) + ) + } + disabled={!hasReadyToUploadPosts} + {...props} + > + {children} + + ); +} diff --git a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/schedule/_components/SubmitBottomCTA/SubmitBottomCTASkeleton.tsx b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/schedule/_components/SubmitBottomCTA/SubmitBottomCTASkeleton.tsx new file mode 100644 index 00000000..082beb9b --- /dev/null +++ b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/schedule/_components/SubmitBottomCTA/SubmitBottomCTASkeleton.tsx @@ -0,0 +1,12 @@ +import { FixedBottomCTA, Icon } from '@repo/ui'; + +export function SubmitBottomCTASkeleton() { + return ( + } + isLoading + > + 예약하러 가기 + + ); +} diff --git a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/schedule/_components/SubmitBottomCTA/index.ts b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/schedule/_components/SubmitBottomCTA/index.ts new file mode 100644 index 00000000..94b7233f --- /dev/null +++ b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/schedule/_components/SubmitBottomCTA/index.ts @@ -0,0 +1,2 @@ +export { SubmitBottomCTA } from './SubmitBottomCTA'; +export { SubmitBottomCTASkeleton } from './SubmitBottomCTASkeleton'; diff --git a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/schedule/page.tsx b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/schedule/page.tsx index 3d24dd4b..53a998ec 100644 --- a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/schedule/page.tsx +++ b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/schedule/page.tsx @@ -3,22 +3,18 @@ import { ServerFetchBoundary } from '@web/store/query/ServerFetchBoundary'; import { getAllPostsQueryOptions } from '@web/store/query/useGetAllPostsQuery'; import { getServerSideTokens } from '@web/shared/server/serverSideTokens'; import { EditPageProps } from '../types'; -import { Suspense } from 'react'; -import Loading from '@web/app/loading'; export default function SchedulePage({ params }: EditPageProps) { const tokens = getServerSideTokens(); const serverFetchOptions = getAllPostsQueryOptions({ - agentId: params.agentId, - postGroupId: params.postGroupId, + agentId: Number(params.agentId), + postGroupId: Number(params.postGroupId), tokens, }); return ( - }> - - + ); } diff --git a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/types.ts b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/types.ts index 3b0f30df..354431d0 100644 --- a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/types.ts +++ b/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/types.ts @@ -3,3 +3,13 @@ import { IdParams } from '@web/types'; export type EditPageProps = { params: Omit; }; + +export type ScheduleFormValues = { + schedules: Array<{ + postId: number; + date: string; + hour: string; + minute: string; + amPm: string; + }>; +}; diff --git a/apps/web/src/app/create/[agentId]/Create.tsx b/apps/web/src/app/create/[agentId]/Create.tsx index f6bcea42..c635efd2 100644 --- a/apps/web/src/app/create/[agentId]/Create.tsx +++ b/apps/web/src/app/create/[agentId]/Create.tsx @@ -12,7 +12,6 @@ import { } from '@repo/ui'; import { AnimatedTitle } from './_components/AnimatedTitle/AnimatedTitle'; import { ImageManager, MainBreadcrumbItem } from '@web/components/common'; -import { KeywordChipGroup } from './_components/KeywordChip/KeywordChipGroup'; import { AnimatedContainer } from './_components/AnimatedContainer/AnimatedContainer'; import { useForm, Controller } from 'react-hook-form'; import { isEmptyStringOrNil } from '@web/utils'; @@ -25,22 +24,28 @@ import { import * as styles from './pageStyle.css'; import { useModal } from '@repo/ui/hooks'; import { useRouter } from 'next/navigation'; -import { useNewsCategoriesQuery } from '@web/store/query/useNewsCategoriesQuery'; import { isNotNil } from '@repo/ui/utils'; import { NavBar } from '@web/components/common'; import { useScroll } from '@web/hooks'; import { useCreatePostsMutation } from '@web/store/mutation/useCreatePostsMutation'; import { uploadImages } from '@web/shared/image-upload/ImageUpload'; import { ROUTES } from '@web/routes'; +import { Suspense } from 'react'; +import { + NewsCategorySection, + NewsCategorySectionSkeleton, +} from './_components/NewsCategorySection'; +import { useClientSidePrefetchNewsCategories } from '@web/store/query/useNewsCategoriesQuery'; const REQUIRED_FIELDS = { TOPIC: 'topic', } as const; export default function Create({ params }: CreatePageProps) { - const { data: newsCategories } = useNewsCategoriesQuery(); + useClientSidePrefetchNewsCategories(); + const { mutate: createPosts, isPending } = useCreatePostsMutation({ - agentId: params.agentId, + agentId: Number(params.agentId), }); const modal = useModal(); const router = useRouter(); @@ -52,8 +57,8 @@ export default function Create({ params }: CreatePageProps) { defaultValues: { topic: '', purpose: 'INFORMATION', + newsCategory: undefined, reference: 'NONE', - newsCategory: newsCategories.data[0]?.category ?? undefined, imageUrls: [], length: 'SHORT', content: '', @@ -76,7 +81,10 @@ export default function Create({ params }: CreatePageProps) { createPosts(requestData); }; - const isSubmitDisabled = isEmptyStringOrNil(topic); + const isSubmitDisabled = + isEmptyStringOrNil(topic) || + (reference === REFERENCE_TYPE.NEWS && + isEmptyStringOrNil(watch('newsCategory'))); const handleImageUpload = async (files: File[]) => { const uploadedUrls = await uploadImages(files); @@ -180,20 +188,9 @@ export default function Create({ params }: CreatePageProps) { {reference === REFERENCE_TYPE.NEWS && (
- ( - ({ - key: category.category, - label: category.name, - }))} - value={value} - onChange={(value) => onChange(value)} - /> - )} - /> + }> + +
)} @@ -263,9 +260,8 @@ export default function Create({ params }: CreatePageProps) {
} - onClick={handleSubmit(onSubmit)} disabled={isSubmitDisabled} isLoading={isPending} > diff --git a/apps/web/src/app/create/[agentId]/_components/NewsCategorySection/NewsCategorySection.tsx b/apps/web/src/app/create/[agentId]/_components/NewsCategorySection/NewsCategorySection.tsx new file mode 100644 index 00000000..2af69a29 --- /dev/null +++ b/apps/web/src/app/create/[agentId]/_components/NewsCategorySection/NewsCategorySection.tsx @@ -0,0 +1,31 @@ +'use client'; + +import { Controller, Control } from 'react-hook-form'; +import { CreateFormValues } from '../../types'; +import { KeywordChipGroup } from '../KeywordChip/KeywordChipGroup'; +import { useNewsCategoriesQuery } from '@web/store/query/useNewsCategoriesQuery'; + +type NewsCategorySectionProps = { + control: Control; +}; + +export function NewsCategorySection({ control }: NewsCategorySectionProps) { + const { data: newsCategories } = useNewsCategoriesQuery(); + + return ( + ( + ({ + key: category.category, + label: category.name, + }))} + value={value} + onChange={(value) => onChange(value)} + /> + )} + /> + ); +} diff --git a/apps/web/src/app/create/[agentId]/_components/NewsCategorySection/NewsCategorySectionSkeleton.tsx b/apps/web/src/app/create/[agentId]/_components/NewsCategorySection/NewsCategorySectionSkeleton.tsx new file mode 100644 index 00000000..ad8d3882 --- /dev/null +++ b/apps/web/src/app/create/[agentId]/_components/NewsCategorySection/NewsCategorySectionSkeleton.tsx @@ -0,0 +1,14 @@ +import { Skeleton } from '@repo/ui'; +import * as style from './style.css'; + +const SKELETON_COUNT = 15; + +export function NewsCategorySectionSkeleton() { + return ( +
+ {Array.from({ length: SKELETON_COUNT }, (_, index) => ( + + ))} +
+ ); +} diff --git a/apps/web/src/app/create/[agentId]/_components/NewsCategorySection/index.ts b/apps/web/src/app/create/[agentId]/_components/NewsCategorySection/index.ts new file mode 100644 index 00000000..a2df09cf --- /dev/null +++ b/apps/web/src/app/create/[agentId]/_components/NewsCategorySection/index.ts @@ -0,0 +1,2 @@ +export { NewsCategorySection } from './NewsCategorySection'; +export { NewsCategorySectionSkeleton } from './NewsCategorySectionSkeleton'; diff --git a/apps/web/src/app/create/[agentId]/_components/NewsCategorySection/style.css.ts b/apps/web/src/app/create/[agentId]/_components/NewsCategorySection/style.css.ts new file mode 100644 index 00000000..54db97b4 --- /dev/null +++ b/apps/web/src/app/create/[agentId]/_components/NewsCategorySection/style.css.ts @@ -0,0 +1,9 @@ +import { vars } from '@repo/theme'; +import { style } from '@vanilla-extract/css'; + +export const skeletonWrapperStyle = style({ + display: 'flex', + gap: vars.space[10], + flexWrap: 'wrap', + width: '100%', +}); diff --git a/apps/web/src/app/create/[agentId]/page.tsx b/apps/web/src/app/create/[agentId]/page.tsx index b1a2a4a0..b6f48919 100644 --- a/apps/web/src/app/create/[agentId]/page.tsx +++ b/apps/web/src/app/create/[agentId]/page.tsx @@ -1,19 +1,6 @@ import Create from './Create'; -import { newsCategoriesQueryOptions } from '@web/store/query/useNewsCategoriesQuery'; -import { ServerFetchBoundary } from '@web/store/query/ServerFetchBoundary'; -import { getServerSideTokens } from '@web/shared/server/serverSideTokens'; import { CreatePageProps } from './types'; -import { Suspense } from 'react'; export default function CreatePage({ params }: CreatePageProps) { - const tokens = getServerSideTokens(); - const serverFetchOptions = newsCategoriesQueryOptions(tokens); - - return ( - - - - - - ); + return ; } diff --git a/apps/web/src/app/loading.tsx b/apps/web/src/app/loading.tsx deleted file mode 100644 index c788406a..00000000 --- a/apps/web/src/app/loading.tsx +++ /dev/null @@ -1,17 +0,0 @@ -'use client'; - -import { DynamicLottie } from '@repo/ui/LottieAnimation'; -import star_loading from '@web/assets/lotties/star_loading.json'; -import { wrapper } from './loading/page.css'; - -export default function Loading() { - return ( -
- -
- ); -} diff --git a/apps/web/src/app/personalize/[agentId]/Personalize.tsx b/apps/web/src/app/personalize/[agentId]/Personalize.tsx index 62370cc2..dfc8f049 100644 --- a/apps/web/src/app/personalize/[agentId]/Personalize.tsx +++ b/apps/web/src/app/personalize/[agentId]/Personalize.tsx @@ -1,96 +1,56 @@ 'use client'; import { useRouter } from 'next/navigation'; -import { - Icon, - Label, - RadioCards, - Spacing, - TextField, - Text, - Breadcrumb, - Dropdown, - Modal, - FixedBottomCTA, -} from '@repo/ui'; -import { - PersonalizeFormValues, - PersonalizePageProps, - TONE_OPTIONS, -} from './type'; +import { Icon, FixedBottomCTA, Spacing, Breadcrumb } from '@repo/ui'; +import { PersonalizeFormValues, PersonalizePageProps } from './type'; import { TitleWithDescription } from '@web/components/common/TitleWithDescription/TitleWithDescription'; import { useForm } from 'react-hook-form'; -import { Controller } from 'react-hook-form'; import { isEmptyStringOrNil } from '@web/utils'; -import { useModal, useToast } from '@repo/ui/hooks'; +import { useToast } from '@repo/ui/hooks'; import { useUpdatePersonalSettingMutation } from '@web/store/mutation/useUpdatePersonalSettingMutation'; import { AccountSidebar } from '@web/components/common/AccountSidebar/AccountSidebar'; import { ROUTES } from '@web/routes'; -import { getAgentQueryOptions } from '@web/store/query/useGetAgentQuery'; import { Agent } from '@web/types'; -import { useQueryClient, useSuspenseQueries } from '@tanstack/react-query'; -import { MainBreadcrumbItem, NavBar } from '@web/components/common'; -import Image from 'next/image'; -import { isNil } from '@repo/ui/utils'; -import { getUserQueryOptions } from '@web/store/query/useGetUserQuery'; +import { useQueryClient } from '@tanstack/react-query'; +import { + MainBreadcrumbItem, + NavBar, + UserProfileDropdown, +} from '@web/components/common'; import { useScroll } from '@web/hooks'; import * as style from './pageStyle.css'; -import { useLogoutMutation } from '@web/store/mutation/useLogoutMutation'; -import { getAgentDetailQueryOptions } from '@web/store/query/useGetAgentDetailQuery'; +import { Suspense } from 'react'; +import { PersonalizeFormContent } from './_components/PersonalizeFormContent/PersonalizeFormContent'; +import { PersonalizeFormContentSkeleton } from './_components/PersonalizeFormContent/PersonalizeFormContentSkeleton'; export default function Personalize({ params }: PersonalizePageProps) { const router = useRouter(); const toast = useToast(); - const modal = useModal(); const [scrollRef, isScrolled] = useScroll({ threshold: 100, }); - const [{ data: agentData }, { data: agentDetail }, { data: user }] = - useSuspenseQueries({ - queries: [ - getAgentQueryOptions(), - getAgentDetailQueryOptions({ agentId: params.agentId }), - getUserQueryOptions(), - ], - }); const { mutate: updatePersonalSetting } = useUpdatePersonalSettingMutation({ - agentId: params.agentId, + agentId: Number(params.agentId), }); - const { mutate: logout } = useLogoutMutation(); const queryClient = useQueryClient(); - const { register, watch, setValue, handleSubmit, control } = - useForm({ - defaultValues: { - domain: agentDetail.agentPersonalSetting.domain, - introduction: agentDetail.agentPersonalSetting.introduction, - tone: agentDetail.agentPersonalSetting.tone, - customTone: agentDetail.agentPersonalSetting.customTone, - }, - }); - const toneValue = watch('tone'); + const methods = useForm({ + defaultValues: { + domain: '', + introduction: '', + tone: 'CASUAL', + customTone: '', + }, + }); + + const { handleSubmit } = methods; const onSubmit = (data: PersonalizeFormValues) => { - if ( - toneValue === TONE_OPTIONS.CUSTOM && - isEmptyStringOrNil(data.customTone) - ) { + if (data.tone === 'CUSTOM' && isEmptyStringOrNil(data.customTone)) { return toast.error('말투를 입력해주세요'); } - const isFormValueChanged = - data.domain !== agentDetail.agentPersonalSetting.domain || - data.introduction !== agentDetail.agentPersonalSetting.introduction || - data.tone !== agentDetail.agentPersonalSetting.tone || - data.customTone !== agentDetail.agentPersonalSetting.customTone; - - if (!isFormValueChanged) { - toast.success('저장되었어요'); - router.push(ROUTES.HOME.DETAIL(params.agentId)); - return; - } - updatePersonalSetting(data); }; @@ -99,20 +59,6 @@ export default function Personalize({ params }: PersonalizePageProps) { router.push(ROUTES.HOME.DETAIL(id)); }; - const handleLogoutClick = () => { - modal.confirm({ - title: '정말 로그아웃 하시겠어요??', - icon: , - confirmButton: '로그아웃', - cancelButton: '취소', - confirmButtonProps: { - onClick: () => { - logout(); - }, - }, - }); - }; - return ( <>
@@ -124,40 +70,11 @@ export default function Personalize({ params }: PersonalizePageProps) { } - rightAddon={ - - - {isNil(user.data.profileImage) ? ( -
- ) : ( - 프로필 - )} - - - - - - 로그아웃 - - - - - } + rightAddon={} isScrolled={isScrolled} />
@@ -172,86 +89,9 @@ export default function Personalize({ params }: PersonalizePageProps) { />
- ( - = 20}> - 활동 분야 - - - )} - /> - - ( - = 500} - > - 계정 소개 - - - )} - /> - -
- - ( - { - onChange(newValue); - if (newValue !== TONE_OPTIONS.CUSTOM) { - setValue('customTone', ''); - } - }} - > - - ~해요 - - - ~합니다 - - - ~해 - - - 직접 입력할게요 - - - )} - /> - {toneValue === TONE_OPTIONS.CUSTOM && ( - = 50} - > - - - )} -
+ }> + +
diff --git a/apps/web/src/app/personalize/[agentId]/_components/PersonalizeFormContent/PersonalizeFormContent.tsx b/apps/web/src/app/personalize/[agentId]/_components/PersonalizeFormContent/PersonalizeFormContent.tsx new file mode 100644 index 00000000..8143f556 --- /dev/null +++ b/apps/web/src/app/personalize/[agentId]/_components/PersonalizeFormContent/PersonalizeFormContent.tsx @@ -0,0 +1,118 @@ +'use client'; + +import { useGetAgentDetailQuery } from '@web/store/query/useGetAgentDetailQuery'; +import { useEffect } from 'react'; +import { Controller, UseFormReturn } from 'react-hook-form'; +import { PersonalizeFormValues, PersonalizePageProps } from '../../type'; +import { Spacing, TextField, Label, RadioCards } from '@repo/ui'; +import { TONE_OPTIONS } from '../../type'; +import * as style from './style.css'; + +type PersonalizeFormContentProps = { + methods: UseFormReturn; + params: PersonalizePageProps['params']; +}; + +export function PersonalizeFormContent({ + methods, + params, +}: PersonalizeFormContentProps) { + const { data: agentDetail } = useGetAgentDetailQuery({ + agentId: Number(params.agentId), + }); + + const { control, register, watch, setValue } = methods; + + useEffect(() => { + if (agentDetail) { + setValue('domain', agentDetail.agentPersonalSetting.domain); + setValue('introduction', agentDetail.agentPersonalSetting.introduction); + setValue('tone', agentDetail.agentPersonalSetting.tone); + setValue('customTone', agentDetail.agentPersonalSetting.customTone); + } + }, [agentDetail, setValue]); + + const toneValue = watch('tone'); + + return ( + <> + ( + = 20}> + 활동 분야 + + + )} + /> + + ( + = 500}> + 계정 소개 + + + )} + /> + +
+ + ( + { + onChange(newValue); + if (newValue !== TONE_OPTIONS.CUSTOM) { + setValue('customTone', ''); + } + }} + > + + ~해요 + + + ~합니다 + + + ~해 + + + 직접 입력할게요 + + + )} + /> + {toneValue === TONE_OPTIONS.CUSTOM && ( + = 50} + > + + + )} +
+ + ); +} diff --git a/apps/web/src/app/personalize/[agentId]/_components/PersonalizeFormContent/PersonalizeFormContentSkeleton.tsx b/apps/web/src/app/personalize/[agentId]/_components/PersonalizeFormContent/PersonalizeFormContentSkeleton.tsx new file mode 100644 index 00000000..e55a831a --- /dev/null +++ b/apps/web/src/app/personalize/[agentId]/_components/PersonalizeFormContent/PersonalizeFormContentSkeleton.tsx @@ -0,0 +1,29 @@ +import { Label, Skeleton, Spacing } from '@repo/ui'; +import * as style from './style.css'; + +export function PersonalizeFormContentSkeleton() { + return ( + <> +
+ + +
+ + +
+ + +
+ + +
+ +
+ {Array.from({ length: 4 }, (_, index) => ( + + ))} +
+
+ + ); +} diff --git a/apps/web/src/app/personalize/[agentId]/_components/PersonalizeFormContent/style.css.ts b/apps/web/src/app/personalize/[agentId]/_components/PersonalizeFormContent/style.css.ts new file mode 100644 index 00000000..52afbdb5 --- /dev/null +++ b/apps/web/src/app/personalize/[agentId]/_components/PersonalizeFormContent/style.css.ts @@ -0,0 +1,23 @@ +import { vars } from '@repo/theme'; +import { style } from '@vanilla-extract/css'; + +export const textFieldWrapperStyle = style({ + position: 'relative', + width: '100%', + display: 'flex', + flexDirection: 'column', + gap: vars.space[8], +}); + +export const utteranceWrapperStyle = style({ + display: 'flex', + flexDirection: 'column', + gap: '1.6rem', + width: '100%', +}); + +export const radioCardsWrapperStyle = style({ + display: 'grid', + gridTemplateColumns: 'repeat(4, 1fr)', + gap: vars.space[16], +}); diff --git a/apps/web/src/app/personalize/[agentId]/page.tsx b/apps/web/src/app/personalize/[agentId]/page.tsx index e68e710c..4b301af6 100644 --- a/apps/web/src/app/personalize/[agentId]/page.tsx +++ b/apps/web/src/app/personalize/[agentId]/page.tsx @@ -8,8 +8,6 @@ import { getAgentQueryOptions } from '@web/store/query/useGetAgentQuery'; import { getUserQueryOptions } from '@web/store/query/useGetUserQuery'; import Personalize from './Personalize'; import { getAgentDetailQueryOptions } from '@web/store/query/useGetAgentDetailQuery'; -import { Suspense } from 'react'; -import Loading from '@web/app/loading'; export default function PersonalizePage({ params }: PersonalizePageProps) { const tokens = getServerSideTokens(); @@ -22,9 +20,7 @@ export default function PersonalizePage({ params }: PersonalizePageProps) { return ( - }> - - + ); } diff --git a/apps/web/src/app/schedule/[agentId]/Schedule.tsx b/apps/web/src/app/schedule/[agentId]/Schedule.tsx index 90cac605..f5b73e48 100644 --- a/apps/web/src/app/schedule/[agentId]/Schedule.tsx +++ b/apps/web/src/app/schedule/[agentId]/Schedule.tsx @@ -1,54 +1,27 @@ 'use client'; import { SchedulePageProps } from './type'; -import { getAgentUploadReservedQueryOptions } from '@web/store/query/useGetAgentUploadReserved'; -import { getAgentQueryOptions } from '@web/store/query/useGetAgentQuery'; -import { getUserQueryOptions } from '@web/store/query/useGetUserQuery'; -import { useQueryClient, useSuspenseQueries } from '@tanstack/react-query'; +import { useQueryClient } from '@tanstack/react-query'; import * as style from './pageStyle.css'; import { NavBar, MainBreadcrumbItem, AccountSidebar, - DndController, + UserProfileDropdown, } from '@web/components/common'; -import { Breadcrumb, Dropdown, Icon, Text, Modal } from '@repo/ui'; -import Image from 'next/image'; +import { Breadcrumb } from '@repo/ui'; import { useScroll } from '@web/hooks'; import { ROUTES } from '@web/routes'; -import { isNil } from '@repo/ui/utils'; -import { useLogoutMutation } from '@web/store/mutation/useLogoutMutation'; -import { Agent, POST_STATUS } from '@web/types'; +import { Agent } from '@web/types'; import { useRouter } from 'next/navigation'; -import { useModal } from '@repo/ui/hooks'; -import { TitleWithDescription } from '@web/components/common/TitleWithDescription/TitleWithDescription'; -import { ScheduleTable } from '@web/components/schedule/ScheduleTable/ScheduleTable'; -import { ContentItem } from '@web/components/common/DNDController/compounds'; -import { FormProvider, useForm } from 'react-hook-form'; -import { getCurrentDateKo } from '@web/app/(prompt)/edit/[agentId]/[postGroupId]/schedule/utils/getCurrentDateKo'; -import { useEffect } from 'react'; -import { parseTime } from '@web/utils'; -import { useUpdateReservedPostsMutation } from '@web/store/mutation/useUpdateReservedPostsMutation'; +import { Suspense } from 'react'; +import { ScheduleContent } from './_components/ScheduleContent/ScheduleContent'; +import { ScheduleContentSkeleton } from './_components/ScheduleContent/ScheduleContentSkeleton'; export default function Schedule({ params }: SchedulePageProps) { - const [{ data: agentData }, { data: user }, { data: reservedPosts }] = - useSuspenseQueries({ - queries: [ - getAgentQueryOptions(), - getUserQueryOptions(), - getAgentUploadReservedQueryOptions({ agentId: params.agentId }), - ], - }); - - const { mutate: updateReservedPosts } = useUpdateReservedPostsMutation( - params.agentId - ); - const { mutate: logout } = useLogoutMutation(); const queryClient = useQueryClient(); - const router = useRouter(); - const modal = useModal(); - const [scrollRef, isScrolled] = useScroll({ + const [scrollRef, isScrolled] = useScroll({ threshold: 100, }); @@ -57,142 +30,28 @@ export default function Schedule({ params }: SchedulePageProps) { router.push(ROUTES.HOME.DETAIL(id)); }; - const handleLogoutClick = () => { - modal.confirm({ - title: '정말 로그아웃 하시겠어요??', - icon: , - confirmButton: '로그아웃', - cancelButton: '취소', - confirmButtonProps: { - onClick: () => { - logout(); - }, - }, - }); - }; - - const methods = useForm({ - defaultValues: { - schedules: reservedPosts.posts.map((post) => { - const parsedTime = parseTime(post.uploadTime); - return { - postId: post.id, - date: parsedTime?.date ?? getCurrentDateKo(), - hour: parsedTime?.hour ?? '00', - minute: parsedTime?.minute ?? '00', - }; - }), - }, - }); - - useEffect(() => { - const subscription = methods.watch((formData) => { - if (!formData.schedules) return; - - const updatePayload = { - posts: formData.schedules.map((schedule, index) => ({ - postId: reservedPosts.posts[index]?.id, - postGroupId: reservedPosts.posts[index]?.postGroupId, - uploadTime: `${schedule?.date}T${schedule?.hour}:${schedule?.minute}:00.000Z`, - })), - }; - updateReservedPosts(updatePayload); - }); - - return () => subscription.unsubscribe(); - }, [methods, reservedPosts.posts, updateReservedPosts]); - return ( - -
- - - - - - } - rightAddon={ - - - {isNil(user.data.profileImage) ? ( -
- ) : ( - 유저 프로필 이미지 - )} - - - - - - 로그아웃 - - - - - } - isScrolled={isScrolled} - /> - -
-
- - `${item.id}-${item.displayOrder}-${item.status}`) - .join(',')} - onDragEnd={(updatedItems) => { - const formValues = methods.getValues('schedules'); - const updatePayload = { - posts: updatedItems[POST_STATUS.UPLOAD_RESERVED].map( - (item, index) => ({ - postId: item.id, - postGroupId: item.postGroupId, - uploadTime: `${formValues?.[index]?.date ?? ''}T${formValues?.[index]?.hour ?? '00'}:${formValues?.[index]?.minute ?? '00'}:00.000Z`, - }) - ), - }; - updateReservedPosts(updatePayload); - }} - renderDragOverlay={(activeItem) => ( - - )} - > - - -
-
- - +
+ + + + + + } + rightAddon={} + isScrolled={isScrolled} + /> + +
+ }> + + +
+
); } diff --git a/apps/web/src/app/schedule/[agentId]/[postGroupId]/[postId]/ScheduleDetail.tsx b/apps/web/src/app/schedule/[agentId]/[postGroupId]/[postId]/ScheduleDetail.tsx index 3f2f9941..3f27232f 100644 --- a/apps/web/src/app/schedule/[agentId]/[postGroupId]/[postId]/ScheduleDetail.tsx +++ b/apps/web/src/app/schedule/[agentId]/[postGroupId]/[postId]/ScheduleDetail.tsx @@ -4,54 +4,29 @@ import * as style from './pageStyle.css'; import { useScroll } from '@web/hooks'; import { useRouter } from 'next/navigation'; import { MainBreadcrumbItem, NavBar } from '@web/components/common'; -import { - Badge, - Breadcrumb, - Dropdown, - Icon, - IconButton, - Modal, - Text, -} from '@repo/ui'; -import Image from 'next/image'; +import { Breadcrumb, Dropdown, Icon, IconButton, Modal, Text } from '@repo/ui'; import { ScheduleDetailPageProps } from './type'; -import { getPostQueryOptions } from '@web/store/query/useGetPostQuery'; -import { isNil } from '@repo/ui/utils'; import { ROUTES } from '@web/routes'; -import { getTopicQueryOptions } from '@web/store/query/useGetTopicQuery'; import { useDeletePostMutation } from '@web/store/mutation/useDeletePostMutation'; import { useModal } from '@repo/ui/hooks'; -import { useSuspenseQueries } from '@tanstack/react-query'; +import { Suspense } from 'react'; +import { BreadcrumbItemContentSkelton } from './_components/BreadcrumbContent/BreadcrumbItemContentSkelton'; +import { BreadcrumbItemContent } from './_components/BreadcrumbContent/BreadcrumbContent'; +import { + ScheduleDetailContent, + ScheduleDetailContentSkeleton, +} from './_components/ScheduleDetailContent'; export default function ScheduleDetail({ params }: ScheduleDetailPageProps) { const [scrollRef, isScrolled] = useScroll({ threshold: 100 }); - const router = useRouter(); const modal = useModal(); - - const [{ data: post }, { data: topic }] = useSuspenseQueries({ - queries: [ - getPostQueryOptions({ - agentId: Number(params.agentId), - postGroupId: Number(params.postGroupId), - postId: Number(params.postId), - }), - getTopicQueryOptions({ - agentId: Number(params.agentId), - postGroupId: Number(params.postGroupId), - }), - ], - }); + const router = useRouter(); const { mutate: deletePost } = useDeletePostMutation({ agentId: Number(params.agentId), postGroupId: Number(params.postGroupId), }); - if (isNil(post)) { - router.push(ROUTES.ERROR); - return; - } - const handleDeletePost = () => { modal.confirm({ title: '정말 삭제하시겠어요?', @@ -75,9 +50,12 @@ export default function ScheduleDetail({ params }: ScheduleDetailPageProps) { leftAddon={ - - {topic.data.topic} - + }> + + } rightAddon={ @@ -109,34 +87,9 @@ export default function ScheduleDetail({ params }: ScheduleDetailPageProps) { isScrolled={isScrolled} />
-
- - {post.data.summary} - - - 요약 - -
- - {post.data.content} - - {post.data.postImages.length > 0 && - post.data.postImages.map((image) => ( -
- {`업로드 -
- ))} + }> + +
); diff --git a/apps/web/src/app/schedule/[agentId]/[postGroupId]/[postId]/_components/BreadcrumbContent/BreadcrumbContent.tsx b/apps/web/src/app/schedule/[agentId]/[postGroupId]/[postId]/_components/BreadcrumbContent/BreadcrumbContent.tsx new file mode 100644 index 00000000..9beae9cd --- /dev/null +++ b/apps/web/src/app/schedule/[agentId]/[postGroupId]/[postId]/_components/BreadcrumbContent/BreadcrumbContent.tsx @@ -0,0 +1,24 @@ +'use client'; + +import { Breadcrumb } from '@repo/ui'; +import * as style from './style.css'; +import { IdParams } from '@web/types'; +import { useGetTopicQuery } from '@web/store/query/useGetTopicQuery'; + +type BreadcrumbItemContentProps = Omit; + +export function BreadcrumbItemContent({ + agentId, + postGroupId, +}: BreadcrumbItemContentProps) { + const { data: topic } = useGetTopicQuery({ + agentId: Number(agentId), + postGroupId: Number(postGroupId), + }); + + return ( + + {topic.data.topic} + + ); +} diff --git a/apps/web/src/app/schedule/[agentId]/[postGroupId]/[postId]/_components/BreadcrumbContent/BreadcrumbItemContentSkelton.tsx b/apps/web/src/app/schedule/[agentId]/[postGroupId]/[postId]/_components/BreadcrumbContent/BreadcrumbItemContentSkelton.tsx new file mode 100644 index 00000000..3c6ec6fe --- /dev/null +++ b/apps/web/src/app/schedule/[agentId]/[postGroupId]/[postId]/_components/BreadcrumbContent/BreadcrumbItemContentSkelton.tsx @@ -0,0 +1,5 @@ +import { Skeleton } from '@repo/ui'; + +export function BreadcrumbItemContentSkelton() { + return ; +} diff --git a/apps/web/src/app/schedule/[agentId]/[postGroupId]/[postId]/_components/BreadcrumbContent/style.css.ts b/apps/web/src/app/schedule/[agentId]/[postGroupId]/[postId]/_components/BreadcrumbContent/style.css.ts new file mode 100644 index 00000000..fb4173df --- /dev/null +++ b/apps/web/src/app/schedule/[agentId]/[postGroupId]/[postId]/_components/BreadcrumbContent/style.css.ts @@ -0,0 +1,8 @@ +import { style } from '@vanilla-extract/css'; + +export const breadcrumbItemStyle = style({ + maxWidth: '120px', + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', +}); diff --git a/apps/web/src/app/schedule/[agentId]/[postGroupId]/[postId]/_components/ScheduleDetailContent/ScheduleDetailContent.tsx b/apps/web/src/app/schedule/[agentId]/[postGroupId]/[postId]/_components/ScheduleDetailContent/ScheduleDetailContent.tsx new file mode 100644 index 00000000..c6d4302c --- /dev/null +++ b/apps/web/src/app/schedule/[agentId]/[postGroupId]/[postId]/_components/ScheduleDetailContent/ScheduleDetailContent.tsx @@ -0,0 +1,58 @@ +import { Badge, Text } from '@repo/ui'; +import Image from 'next/image'; +import * as style from './style.css'; +import { useGetPostQuery } from '@web/store/query/useGetPostQuery'; +import { ScheduleDetailPageProps } from '../../type'; +import { useRouter } from 'next/navigation'; +import { isNil } from '@repo/ui/utils'; +import { ROUTES } from '@web/routes'; + +type ScheduleDetailContentProps = ScheduleDetailPageProps; + +export function ScheduleDetailContent({ params }: ScheduleDetailContentProps) { + const { data: post } = useGetPostQuery({ + agentId: Number(params.agentId), + postGroupId: Number(params.postGroupId), + postId: Number(params.postId), + }); + + const router = useRouter(); + + if (isNil(post)) { + router.push(ROUTES.ERROR); + return; + } + + return ( + <> +
+ + {post.data.summary} + + + 요약 + +
+ + {post.data.content} + + {post.data.postImages.length > 0 && + post.data.postImages.map((image) => ( +
+ {`업로드 +
+ ))} + + ); +} diff --git a/apps/web/src/app/schedule/[agentId]/[postGroupId]/[postId]/_components/ScheduleDetailContent/ScheduleDetailContentSkeleton.tsx b/apps/web/src/app/schedule/[agentId]/[postGroupId]/[postId]/_components/ScheduleDetailContent/ScheduleDetailContentSkeleton.tsx new file mode 100644 index 00000000..88902276 --- /dev/null +++ b/apps/web/src/app/schedule/[agentId]/[postGroupId]/[postId]/_components/ScheduleDetailContent/ScheduleDetailContentSkeleton.tsx @@ -0,0 +1,21 @@ +import { Skeleton, Spacing } from '@repo/ui'; +import * as style from './style.css'; + +const SKELETON_COUNT = 4; + +export function ScheduleDetailContentSkeleton() { + return ( + <> + + + + + +
+ {Array.from({ length: SKELETON_COUNT }).map((_, index) => ( + + ))} +
+ + ); +} diff --git a/apps/web/src/app/schedule/[agentId]/[postGroupId]/[postId]/_components/ScheduleDetailContent/index.ts b/apps/web/src/app/schedule/[agentId]/[postGroupId]/[postId]/_components/ScheduleDetailContent/index.ts new file mode 100644 index 00000000..e2a892fc --- /dev/null +++ b/apps/web/src/app/schedule/[agentId]/[postGroupId]/[postId]/_components/ScheduleDetailContent/index.ts @@ -0,0 +1,2 @@ +export { ScheduleDetailContent } from './ScheduleDetailContent'; +export { ScheduleDetailContentSkeleton } from './ScheduleDetailContentSkeleton'; diff --git a/apps/web/src/app/schedule/[agentId]/[postGroupId]/[postId]/_components/ScheduleDetailContent/style.css.ts b/apps/web/src/app/schedule/[agentId]/[postGroupId]/[postId]/_components/ScheduleDetailContent/style.css.ts new file mode 100644 index 00000000..8b0c8cc4 --- /dev/null +++ b/apps/web/src/app/schedule/[agentId]/[postGroupId]/[postId]/_components/ScheduleDetailContent/style.css.ts @@ -0,0 +1,29 @@ +import { style } from '@vanilla-extract/css'; +import { vars } from '@repo/theme'; + +export const titleSectionStyle = style({ + width: '100%', + display: 'flex', + flexDirection: 'row', + alignItems: 'flex-start', + gap: vars.space[16], + padding: `${vars.space[24]} ${vars.space[12]}`, +}); + +export const contentStyle = style({ + width: '100%', + padding: `0 ${vars.space[12]}`, +}); + +export const imageWrapperStyle = style({ + width: '100%', + display: 'flex', + flexDirection: 'row', + gap: vars.space[10], +}); + +export const imageStyle = style({ + width: '100%', + height: 'auto', + borderRadius: vars.borderRadius[16], +}); diff --git a/apps/web/src/app/schedule/[agentId]/[postGroupId]/[postId]/page.tsx b/apps/web/src/app/schedule/[agentId]/[postGroupId]/[postId]/page.tsx index 8d2070f1..abb094de 100644 --- a/apps/web/src/app/schedule/[agentId]/[postGroupId]/[postId]/page.tsx +++ b/apps/web/src/app/schedule/[agentId]/[postGroupId]/[postId]/page.tsx @@ -7,8 +7,6 @@ import { getServerSideTokens } from '@web/shared/server/serverSideTokens'; import { ScheduleDetailPageProps } from './type'; import { getPostQueryOptions } from '@web/store/query/useGetPostQuery'; import { getTopicQueryOptions } from '@web/store/query/useGetTopicQuery'; -import { Suspense } from 'react'; -import Loading from '@web/app/loading'; export default function ScheduleDetailPage({ params, @@ -16,23 +14,21 @@ export default function ScheduleDetailPage({ const tokens = getServerSideTokens(); const serverFetchOptions = [ getPostQueryOptions({ - agentId: params.agentId, - postGroupId: params.postGroupId, - postId: params.postId, + agentId: Number(params.agentId), + postGroupId: Number(params.postGroupId), + postId: Number(params.postId), tokens, }), getTopicQueryOptions({ - agentId: params.agentId, - postGroupId: params.postGroupId, + agentId: Number(params.agentId), + postGroupId: Number(params.postGroupId), tokens, }), ] as FetchOptions[]; return ( - }> - - + ); } diff --git a/apps/web/src/app/schedule/[agentId]/[postGroupId]/[postId]/pageStyle.css.ts b/apps/web/src/app/schedule/[agentId]/[postGroupId]/[postId]/pageStyle.css.ts index d07b61fe..d56e2f9c 100644 --- a/apps/web/src/app/schedule/[agentId]/[postGroupId]/[postId]/pageStyle.css.ts +++ b/apps/web/src/app/schedule/[agentId]/[postGroupId]/[postId]/pageStyle.css.ts @@ -10,40 +10,13 @@ export const mainStyle = style({ }); export const contentWrapperStyle = style({ - maxWidth: '768px', + maxWidth: '76.8rem', position: 'relative', display: 'flex', flexDirection: 'column', padding: `0 ${vars.space[16]}`, }); -export const titleSectionStyle = style({ - width: '100%', - display: 'flex', - flexDirection: 'row', - alignItems: 'flex-start', - gap: vars.space[16], - padding: `${vars.space[24]} ${vars.space[12]}`, -}); - -export const contentStyle = style({ - width: '100%', - padding: `0 ${vars.space[12]}`, -}); - -export const imageWrapperStyle = style({ - width: '100%', - display: 'flex', - flexDirection: 'column', - gap: vars.space[10], -}); - -export const imageStyle = style({ - width: '100%', - height: 'auto', - borderRadius: vars.borderRadius[16], -}); - export const buttonWrapperStyle = style({ display: 'flex', alignItems: 'center', diff --git a/apps/web/src/app/schedule/[agentId]/_components/ScheduleContent/ScheduleContent.tsx b/apps/web/src/app/schedule/[agentId]/_components/ScheduleContent/ScheduleContent.tsx new file mode 100644 index 00000000..83167c72 --- /dev/null +++ b/apps/web/src/app/schedule/[agentId]/_components/ScheduleContent/ScheduleContent.tsx @@ -0,0 +1,116 @@ +'use client'; + +import { + getAgentUploadReservedQueryOptions, + useGetAgentUploadReservedQuery, +} from '@web/store/query/useGetAgentUploadReserved'; +import { DndController } from '@web/components/common'; +import { IdParams, Post, POST_STATUS } from '@web/types'; +import { TitleWithDescription } from '@web/components/common/TitleWithDescription/TitleWithDescription'; +import { ScheduleTable } from '@web/components/schedule/ScheduleTable/ScheduleTable'; +import { ContentItem } from '@web/components/common/DNDController/compounds'; +import { useForm, FormProvider } from 'react-hook-form'; +import { useUpdateReservedPostsMutation } from '@web/store/mutation/useUpdateReservedPostsMutation'; +import * as style from './style.css'; +import { parseTime } from '@web/utils'; +import { getCurrentDateKo } from '@web/utils'; +import { ScheduleFormValues } from '../../type'; +import { useQueryClient } from '@tanstack/react-query'; + +type ScheduleContentProps = { + agentId: IdParams['agentId']; +}; + +export function ScheduleContent({ agentId }: ScheduleContentProps) { + const queryClient = useQueryClient(); + + const { data: reservedPosts } = useGetAgentUploadReservedQuery({ + agentId: Number(agentId), + }); + + const { mutate: updateReservedPosts } = useUpdateReservedPostsMutation( + Number(agentId) + ); + + const methods = useForm({ + defaultValues: { + schedules: reservedPosts.posts.map(mapPostToSchedule), + }, + }); + + if (!reservedPosts) return null; + + return ( + +
+ + `${item.id}-${item.displayOrder}-${item.status}`) + .join(',')} + onDragEnd={(updatedItems) => { + const formValues = methods.getValues('schedules'); + console.log(updatedItems); + const updatePayload = { + posts: updatedItems[POST_STATUS.UPLOAD_CONFIRMED] + .map((item, index) => { + const schedule = formValues[index]; + if (!schedule) return null; + + return { + postId: item.id, + postGroupId: item.postGroupId, + uploadTime: `${schedule.date}T${schedule.hour}:${schedule.minute}:00.000Z`, + }; + }) + .filter( + (item): item is NonNullable => item !== null + ), + }; + updateReservedPosts(updatePayload, { + onSuccess: () => { + queryClient.invalidateQueries( + getAgentUploadReservedQueryOptions({ + agentId: Number(agentId), + }) + ); + }, + }); + }} + renderDragOverlay={(activeItem) => } + > + + + +
+ ); +} + +function mapPostToSchedule(post: Post) { + const parsedTime = parseTime(post.uploadTime); + return { + ...post, + postId: post.id, + date: parsedTime?.date || getCurrentDateKo() || '', + hour: parsedTime?.hour ?? '12', + minute: parsedTime?.minute ?? '00', + amPm: parsedTime?.amPm ?? '오전', + }; +} diff --git a/apps/web/src/app/schedule/[agentId]/_components/ScheduleContent/ScheduleContentSkeleton.tsx b/apps/web/src/app/schedule/[agentId]/_components/ScheduleContent/ScheduleContentSkeleton.tsx new file mode 100644 index 00000000..da86ee7d --- /dev/null +++ b/apps/web/src/app/schedule/[agentId]/_components/ScheduleContent/ScheduleContentSkeleton.tsx @@ -0,0 +1,28 @@ +import { Skeleton, Spacing } from '@repo/ui'; +import * as style from './style.css'; +import { TitleWithDescription } from '@web/components/common/TitleWithDescription/TitleWithDescription'; +import { columns } from '@web/components/schedule/ScheduleTable/constants'; +import { Table } from '@web/components/common'; + +export function ScheduleContentSkeleton() { + const totalWidth = columns.reduce((acc, column) => { + const width = parseFloat(column.width); + return acc + width; + }, 0); + + return ( +
+ } + description="개별 글의 업로드 날짜와 순서를 변경할 수 있어요" + /> +
+ }> + + +
+
+
+ ); +} diff --git a/apps/web/src/app/schedule/[agentId]/_components/ScheduleContent/style.css.ts b/apps/web/src/app/schedule/[agentId]/_components/ScheduleContent/style.css.ts new file mode 100644 index 00000000..bff8b838 --- /dev/null +++ b/apps/web/src/app/schedule/[agentId]/_components/ScheduleContent/style.css.ts @@ -0,0 +1,55 @@ +import { style } from '@vanilla-extract/css'; +import { vars } from '@repo/theme'; + +export const mainStyle = style({ + position: 'relative', + minHeight: '100vh', + padding: '8rem 0 12rem', + display: 'flex', + overflowX: 'auto', + backgroundColor: vars.colors.grey, +}); + +export const image = style({ + borderRadius: '100%', + width: '4rem', + height: '4rem', + backgroundColor: vars.colors.grey25, + border: `0.1rem solid ${vars.colors.grey200}`, + objectFit: 'cover', +}); + +export const dropdownItem = style({ + padding: '1.45rem 1.6rem', + display: 'flex', + alignItems: 'center', + gap: '1rem', +}); + +export const contentWrapperStyle = style({ + position: 'relative', + flex: 1, + padding: `0 ${vars.space[40]}`, + overflow: 'auto', + display: 'flex', + flexDirection: 'column', + marginLeft: '44rem', + //maxWidth: '144rem', +}); + +export const dndSectionStyle = style({ + width: '100rem', + position: 'relative', + margin: '0 auto', +}); + +export const titleWrapperStyle = style({ + display: 'flex', + flexDirection: 'row', + gap: vars.space[8], +}); + +export const tableContainer = style({ + position: 'relative', + width: '100%', +}); diff --git a/apps/web/src/app/schedule/[agentId]/page.tsx b/apps/web/src/app/schedule/[agentId]/page.tsx index b2d2bc53..424c19f7 100644 --- a/apps/web/src/app/schedule/[agentId]/page.tsx +++ b/apps/web/src/app/schedule/[agentId]/page.tsx @@ -1,4 +1,3 @@ -import { Suspense } from 'react'; import Schedule from './Schedule'; import { SchedulePageProps } from './type'; import { getServerSideTokens } from '@web/shared/server/serverSideTokens'; @@ -9,7 +8,6 @@ import { ServerFetchBoundary, } from '@web/store/query/ServerFetchBoundary'; import { getAgentUploadReservedQueryOptions } from '@web/store/query/useGetAgentUploadReserved'; -import Loading from '@web/app/loading'; export default function SchedulePage({ params }: SchedulePageProps) { const tokens = getServerSideTokens(); @@ -17,14 +15,15 @@ export default function SchedulePage({ params }: SchedulePageProps) { const serverFetchOptions = [ getAgentQueryOptions(tokens), getUserQueryOptions(tokens), - getAgentUploadReservedQueryOptions({ agentId: params.agentId, tokens }), + getAgentUploadReservedQueryOptions({ + agentId: Number(params.agentId), + tokens, + }), ] as FetchOptions[]; return ( - }> - - + ); } diff --git a/apps/web/src/app/schedule/[agentId]/type.ts b/apps/web/src/app/schedule/[agentId]/type.ts index 910da6b2..432c01d9 100644 --- a/apps/web/src/app/schedule/[agentId]/type.ts +++ b/apps/web/src/app/schedule/[agentId]/type.ts @@ -3,3 +3,12 @@ import { IdParams } from '@web/types'; export type SchedulePageProps = { params: Pick; }; + +export type ScheduleFormValues = { + schedules: Array<{ + postId: number; + date: string; + hour: string; + minute: string; + }>; +}; diff --git a/apps/web/src/components/common/AccountSidebar/AccountSidebar.css.ts b/apps/web/src/components/common/AccountSidebar/AccountSidebar.css.ts index 99aabe3d..b5286ae6 100644 --- a/apps/web/src/components/common/AccountSidebar/AccountSidebar.css.ts +++ b/apps/web/src/components/common/AccountSidebar/AccountSidebar.css.ts @@ -1,6 +1,6 @@ import { style } from '@vanilla-extract/css'; -export const wrapper = style({ +export const wrapperStyle = style({ position: 'fixed', zIndex: 100, top: 0, @@ -14,9 +14,29 @@ export const wrapper = style({ 'linear-gradient(0deg, #F6F7FC 0%, #F6F7FC 100%), linear-gradient(180deg, #F8F8FF 0%, #F4F5F9 48.16%, #E9F0FA 84.19%)', }); -export const titleWrapper = style({ +export const titleWrapperStyle = style({ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '2.65rem 1.2rem 1.45rem 1.2rem', }); + +export const skeletonListWrapperStyle = style({ + display: 'flex', + flexDirection: 'column', + gap: '1.6rem', +}); + +export const skeletonWrapperStyle = style({ + display: 'flex', + gap: '2rem', + alignItems: 'center', + padding: '1.6rem 1.2rem', +}); + +export const skeletonTitleWrapperStyle = style({ + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + gap: '0.4rem', +}); diff --git a/apps/web/src/components/common/AccountSidebar/AccountSidebar.tsx b/apps/web/src/components/common/AccountSidebar/AccountSidebar.tsx index 9cacbb65..9ebdc7ed 100644 --- a/apps/web/src/components/common/AccountSidebar/AccountSidebar.tsx +++ b/apps/web/src/components/common/AccountSidebar/AccountSidebar.tsx @@ -1,61 +1,73 @@ 'use client'; import { Text } from '@repo/ui/Text'; -import { wrapper, titleWrapper } from './AccountSidebar.css'; +import * as style from './AccountSidebar.css'; import { AccountItem } from './AccountItem/AccountItem'; import { Agent } from '@web/types'; import { useGetXLoginQuery } from '@web/store/query/useGetXLogin'; import { IconButton } from '@repo/ui/IconButton'; import { isNotNil } from '@repo/ui/utils'; +import { useGetAgentQuery } from '@web/store/query/useGetAgentQuery'; +import { Suspense } from 'react'; +import { AccountSidebarSkeleton } from './AccountSidebarSkeleton'; -export type AccountSidebarProps = { - agentData: Agent[]; +type AccountSidebarProps = { selectedId?: Agent['id']; onAccountClick: (id: Agent['id']) => void; }; -/** - * TODO: position fixed 적용 필요 - */ export function AccountSidebar({ - agentData, selectedId, onAccountClick, }: AccountSidebarProps) { - const { refetch } = useGetXLoginQuery(); + const { refetch: refetchXLogin } = useGetXLoginQuery(); const handleClick = async () => { - const result = await refetch(); + const result = await refetchXLogin(); if (result.data?.data.redirectUrl) { window.location.href = result.data.data.redirectUrl; } }; return ( -
-
+
+
내 계정
- {agentData.length > 0 ? ( - agentData.map((data) => ( - onAccountClick(data.id)} - /> - )) - ) : ( - - )} + }> + +
); } + +function AccountSidebarContent({ + selectedId, + onAccountClick, +}: AccountSidebarProps) { + const { data: agentData } = useGetAgentQuery(); + + return agentData.agents.length > 0 ? ( + agentData.agents.map((data) => ( + onAccountClick(data.id)} + /> + )) + ) : ( + + ); +} diff --git a/apps/web/src/components/common/AccountSidebar/AccountSidebarSkeleton.tsx b/apps/web/src/components/common/AccountSidebar/AccountSidebarSkeleton.tsx new file mode 100644 index 00000000..32d49d7b --- /dev/null +++ b/apps/web/src/components/common/AccountSidebar/AccountSidebarSkeleton.tsx @@ -0,0 +1,24 @@ +import { Skeleton } from '@repo/ui'; +import * as style from './AccountSidebar.css'; + +export function AccountSidebarSkeleton() { + return ( +
+ + + +
+ ); +} + +function SkeletonItem() { + return ( +
+ +
+ + +
+
+ ); +} diff --git a/apps/web/src/components/common/BreadcrumbContent/BreadcrumbContent.tsx b/apps/web/src/components/common/BreadcrumbContent/BreadcrumbContent.tsx new file mode 100644 index 00000000..dbf4a010 --- /dev/null +++ b/apps/web/src/components/common/BreadcrumbContent/BreadcrumbContent.tsx @@ -0,0 +1,24 @@ +'use client'; + +import { Breadcrumb } from '@repo/ui'; +import * as style from './style.css'; +import { useGetAllPostsQuery } from '@web/store/query/useGetAllPostsQuery'; +import { IdParams } from '@web/types'; + +type BreadcrumbItemContentProps = Omit; + +export function BreadcrumbItemContent({ + agentId, + postGroupId, +}: BreadcrumbItemContentProps) { + const { data: posts } = useGetAllPostsQuery({ + agentId: Number(agentId), + postGroupId: Number(postGroupId), + }); + + return ( + + {posts.data.postGroup.topic} + + ); +} diff --git a/apps/web/src/components/common/BreadcrumbContent/BreadcrumbItemContentSkelton.tsx b/apps/web/src/components/common/BreadcrumbContent/BreadcrumbItemContentSkelton.tsx new file mode 100644 index 00000000..3c6ec6fe --- /dev/null +++ b/apps/web/src/components/common/BreadcrumbContent/BreadcrumbItemContentSkelton.tsx @@ -0,0 +1,5 @@ +import { Skeleton } from '@repo/ui'; + +export function BreadcrumbItemContentSkelton() { + return ; +} diff --git a/apps/web/src/components/common/BreadcrumbContent/style.css.ts b/apps/web/src/components/common/BreadcrumbContent/style.css.ts new file mode 100644 index 00000000..fb4173df --- /dev/null +++ b/apps/web/src/components/common/BreadcrumbContent/style.css.ts @@ -0,0 +1,8 @@ +import { style } from '@vanilla-extract/css'; + +export const breadcrumbItemStyle = style({ + maxWidth: '120px', + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', +}); diff --git a/apps/web/src/components/common/TitleWithDescription/TitleWithDescription.tsx b/apps/web/src/components/common/TitleWithDescription/TitleWithDescription.tsx index 9ed451bc..c33a8b14 100644 --- a/apps/web/src/components/common/TitleWithDescription/TitleWithDescription.tsx +++ b/apps/web/src/components/common/TitleWithDescription/TitleWithDescription.tsx @@ -1,9 +1,10 @@ import { Text } from '@repo/ui'; import * as style from './TitleWithDescription.css'; +import { ReactNode } from 'react'; type TitleWithDescriptionProps = { title: string; - rightTitle?: string; + rightTitle?: ReactNode; description: string; }; diff --git a/apps/web/src/components/common/UserProfileDropdown/UserProfileDropdown.css.ts b/apps/web/src/components/common/UserProfileDropdown/UserProfileDropdown.css.ts new file mode 100644 index 00000000..ec06726e --- /dev/null +++ b/apps/web/src/components/common/UserProfileDropdown/UserProfileDropdown.css.ts @@ -0,0 +1,14 @@ +import { style } from '@vanilla-extract/css'; + +export const imageStyle = style({ + width: '4rem', + height: '4rem', + borderRadius: '50%', + backgroundColor: 'grey200', +}); + +export const dropdownItemStyle = style({ + display: 'flex', + alignItems: 'center', + gap: '1rem', +}); diff --git a/apps/web/src/components/common/UserProfileDropdown/UserProfileDropdown.tsx b/apps/web/src/components/common/UserProfileDropdown/UserProfileDropdown.tsx new file mode 100644 index 00000000..cc1f91c3 --- /dev/null +++ b/apps/web/src/components/common/UserProfileDropdown/UserProfileDropdown.tsx @@ -0,0 +1,95 @@ +// components/common/UserProfileDropdown/UserProfileDropdown.tsx +'use client'; + +import { Dropdown, Icon, Skeleton, Spacing, Text } from '@repo/ui'; +import Image from 'next/image'; +import { useGetUserQuery } from '@web/store/query/useGetUserQuery'; +import { useLogoutMutation } from '@web/store/mutation/useLogoutMutation'; +import { useModal } from '@repo/ui/hooks'; +import { isNil } from '@repo/ui/utils'; +import { Suspense } from 'react'; +import * as style from './UserProfileDropdown.css'; +import iconNotice from './assets/iconNotice.png'; + +export function UserProfileDropdown() { + return ( + }> + + + ); +} + +function UserProfileDropdownContent() { + const { data: user } = useGetUserQuery(); + const { mutate: logout } = useLogoutMutation(); + const modal = useModal(); + + const handleLogoutClick = () => { + modal.confirm({ + title: '로그아웃 하시겠어요?', + icon: ( + <> + 로그아웃 알림 아이콘 + + + ), + confirmButton: '로그아웃', + cancelButton: '취소', + confirmButtonProps: { + onClick: () => { + logout(); + }, + }, + }); + }; + + return ( + + + {isNil(user.data.profileImage) ? ( +
+ ) : ( + 프로필 + )} + + + + + + 로그아웃 + + + + + ); +} + +function UserProfileDropdownSkeleton() { + return ( + + + + + + + + + + + ); +} diff --git a/apps/web/src/components/common/UserProfileDropdown/assets/iconNotice.png b/apps/web/src/components/common/UserProfileDropdown/assets/iconNotice.png new file mode 100644 index 0000000000000000000000000000000000000000..7613671fb0ae784b0fdf18d909b22e111c264400 GIT binary patch literal 5724 zcmZu#c{CK>`@XX=MzUpzELk(MRfZOfY$5wTwjm^Y`4~GxLaB(znnCt`FJeX#DcKqO zC@D+Xmt^_r`}gmj^Pc;>&)weloO{o?@AJ@5UyBLFg8~3BY2Q#c1^|NnXGpqZrY9l4 zIIM%e)RZRmR zzora)%+1m}*M3aC$Iwhq)3J38ksR`Llg9j-SM!^K70@0{F6QblY;6(GJ-r{KPyJ_w zO&+f0ZTE7+z<*`o-bxQmiTu`7P+v$t7cN%a^q}tTic;v8mK8<@wgBIiH#?cZzjsVH zAQLCivp+U6f9$hDj)VFC8ZwrR0%-~JGscLEIor5sENE}!%cmVN&v%~Qs{7fN9egHV z;q%d2a~m_JM!?;wCyaV=M&HA$C{zmrKlc#4zp*(!UiSEjp3Xq_r|Wv+z3KQ14!2=n z;29%%4>GJJ=i8(5a2&~D&WCp9wWKpMHfIIpP*9c$r(~O>k53sC+3zhP*zft~4wV%W zCH2#~jh3znYA9oloFw7G{OX{=l9SN#@bZQ;UU^QGohR!9V#@1#8&&>jGdn}TkD-Gg z46H>ee~UeJPR_c~8S>=q*+c=g?o`K_G zKY87=LK1Q~Ee9Dhr(A?OGu znPIk(eFD%b7q|eSPZYVN1xBK1?Xf9NHrXyus`%mfj?KOCvdyH%&+7iFL?gNL#R!Nc zYeOxC$xrixw>|AYZFwXG^>fLcF;V0PQd!Yqhqb^ZG=XXO3oG&D!q$@SfNn3tQb8T2!MZZd^D#uo|J$(gIJK>1$m(z7_8sWb7i$rz~ zG*)Ckw`;n&dp2O(y{@ogk$0w5G~hg%qQzbJ_C-`^sH@l)4i!4cYZ%T>{VZ>SNM~U7 z+^lPaKx7J6f0cbZ+O|M6Oc70yN?jN7`WPrLAByu~hA1Z3S`V8LTDS#+zIOmAyKvx(hK|I1v`FA)F3A%V1~$yoM4R!RHGYPvvq)`cy*<8x^GuT|@St5D*Y`akxIkrhX&@|3^4Sqv(aByH^hUAc62CHpbm7YJdu!Syf2Ox_O3~QrOD&mK<35v zCrC=0T;nY$iQ$T#74wNOCRz8;;KdT)nnG7bPiG66=gKINgpvYzs`$AHjFS@^oJ(O? zHRq)WhZW`%4TzD^QBL*ZqTl?macu!JlZ&q07gpL-e8a_%Zr_9nbGav@9CDl?;xJZohEsg`MqZk=fKNc7LSc% zDmG9~?p`J&K8`k=F2Tw`QpJ@mCbfRtXU@Mz&}DKQTr)!il>iR+aJjv8-QvkY4)fI2 zE(5V4e&YRCE6A^o)`OMq)C$K5k8v7zU@=t5lxP}mwAb{ZB?tp&&DpEX83C{M;;30X zr57-ooh94os(Dnxq71H40dcN!`f-sIs0|G_4F~+?Bj~DDRDy{B(9B6>H%`Jq|A$=+ zVum(JK}sBg!ve&qqJ@IgbR2$)6>yv7>W>&9%=w99p1qSh^mu=<{Kng=X!B4+QDPDl zH1Cv{mJNWR(W6VZ4;95hRdBNR!{P?$&6lJ-m|UK=-cKy?+iw3UeIIf(0tLRTtuDWh74Zp5SlmIl#Q7A7eO;DEWx}^N3BZF(o=FC z=|`Kg6PFKQG~-;mM3f>k7zNX9JEF*dZ6Jf9ctXR;p9Rom@hAb}-llkF zU0Vf?)-50dAqE>O@D*l&q3Z}FN%`prM3)}Fy-mLq4&1)G@KuE%s1qXlV5qk(49g|u zqXkF=bQCaMK7tvNFa5fqEe%cJkdlf1)SMsnIhTWDdMm z%N0q=Lk2W<`7_rs4Y+e zZ}X_TP`Va+?Ms}>ClwEtu(kZI(NXmZ+lj+bDq-xmaL8JrhhLjChf2D&5ZJQFMR6s# z<~gvO9OE3z?a(-_Q?8@h0ZSMZJQqj1&v@lP2Zat}ivn<#NH#0eYhdv3(ea%b*8xs!?IX{E*Js{Xvirz`70^1dz!8m4QK|%`LFLmiUxZTAVhocy2UJWG9xyh zI|{6dYd0BR9|JZVU{y2NBcd~G;27nFsua5e7IFOf4#FUL$FB_ z7%I@KS#re-g`Nk7jj#4uMl=vzBHTn~TZ!Tj@YH~X2o^2w0mtN%ls3UA7Pz5sy)gke zz6s>E-Lj&G8w!Z(Kay5}+&F%Zb}u?0;{M}HTU*)-2Y$9htv8|8PJyD~>~kYWKU5l) zg>U>lQa<+5Zoih`i60%*8v6XX>k$6*i9pO`9Ti)Nd&&(r{qbj`ltvNUdQ#Ahmpf4LU*mUeaG>9C-wWxZSQtjfs=}-_40BK zm&pF)=|)}*{AeSW)KN#+L(njwGu!4=oe5^)z!cs1_WXK8UcJU80~H!d*8Bd{AyY17 zxoIXi{P{R_LAKn7v6zD^_^9FD6qGmp)?LqAl4P{?4E@+@9?IkFuaJZGP_XS})F+)C zCRYD90eED+v}5bcq^FV8f1$?Pya*Mwd%QJfv?6g`7Rb)8N;6ePkAAww4>g@x21o4` zr6YX{h+?cfL{a&!UEj&kLy_s-{WnIHVe+=lqL8jjuQf`jo7YOL`ElO6edW0a^=P+< ziyn`>VA&H!%iFx~4VJnIWD73hSb^*G2|B~>=xfyrA$wO%t$>SX)uOML1VP!`b(WC2 zD^Wl9^;dkB=@>L)z|1UV5*gLYGTz%G+plXTIyjS~1M~5@ZDGMu`Qs(K_sKIX?{AhD zEiCBlTrsnT%yUP-UruZP+Oy9X8EaOz<6XK4OK`X`xjezwN^MkQ5tl0c_YXmv2ZH%v zXsN-y0LM`f3V(KR`3>I{yN8Q9OpC4&mrt`bTO3)r{rL7nJ@j|M?R>g=IF06nWS_Mh zb1VCT?rWH;Bd-~|RhJ|QFqafUKf8|8Qd?RLdsDb4wp-U92|CJ~g`0aqx>8cR#_&N0 zQKrA`!#c+8C!73^`6nEte0Nkf0s;Nv@?Fts%aVHxE%RUXP9@}TUn4rfD->LC z`^_g|Kd;cSl6g}4=ik9u0$S#&}*B8T=D;_p~XMV2PZ*Eq1VjAQyNiy-A8S2 zB|(9-e-Km-%9jhcvtDunDaR6( z-r3uMl^}-739I|?DE-Q*_c>lG95kgXIVkuzsjy}FPjD(8DjR&DEyn`M<)qMVmEv)D zN#5AWgQc23&FLKAE`!r*wV9$+)d@Nw9kLh@I`!dr5((PZtsmQmg59kaS9S}Sztlrt!Za*4x~0{=AQDo zpP+8@xTY*%v*83iU;h-mFoI7&nVorGJ*a($&4V}028y>?5Y;j{=y zqPwWomuuqjx7|_TopX5nw5NXONGht&QHceasXxErTUPs)|zRof)6~5nJAw&=!`Yn)@SYX{$*qE0+k(pY(G}ft0i{;*1-m zqd=p>0@97otDDOFsCkE!IeTA|H9DKa8KL-Ubi~E^rfm)z9EIGKSFp%EiH&1{qg+*Y zt@`On8c1k!U*c*lj+Mx%&;^9KZwBA8-3T^(u9gtm zva@8e9gCpmc!=kOr?eE6b`6cA#o!eqky+*@F<%%P&I1307De#|(Gf(yYuSzxo7C#b z8kyAP7L$4r>_xxY;C&>S&j|9An8MAZ{^=?d>Z9(FtSa2M{u?E=8e8*mjNRbNMN!SF zbqN`%6=_2VXF?W#wUpej;B|bda=yLEaYfix@sH*c=*H+T#X)y3mF*$P*NcE|E>9Gf zX{f#-B;~o;eS&AgQoSKpiSq3tz}GF-@$I8%#gvvtQ*P4WiOZ8`fh5-YMC0qG5;PpI zv96VhF{qB<=}4oo5B@%M-U!KN97V*p?p|$K6v<`qU#eECk#WE164t z=bJm>RIAaGL)<g_XxBPlKo&S8w(ukC~c`QuL ztt4$4q{)UP(@(11Ht^h(NjLX@h*1cE)kJYh*&az+fToCSvHi@KL^Dj2w4wi4cID?d|=2CGRti)$;>`xDfH9LsE9=B7` z20aK`G)vVrjma~XbV*pN=5tEjCOqhiFNoi9gUSU)V9)Tt z?HpSVHs;wPMvrByIA@dZq%hna zb{`#Yr^>uL>|W>tbm{MbYL0~+h15RBQ*gn|F6cc+N8=ClKVH<5KEIQzJX3!_SYGH? zwm!OQQtf#{x!3LRM ); } - -const columns: Column[] = [ - { - id: 'date', - label: '날짜 변경', - width: '17rem', - }, - { - id: 'amPm', - label: '오전/오후', - width: '10.8rem', - }, - { - id: 'time', - label: '시', - width: '10.8rem', - }, - { - id: 'minute', - label: '분', - width: '10.8rem', - }, - { - id: 'action', - label: '순서 변경', - width: '52.5rem', - }, -]; diff --git a/apps/web/src/components/schedule/ScheduleTable/constants.ts b/apps/web/src/components/schedule/ScheduleTable/constants.ts new file mode 100644 index 00000000..26aa821d --- /dev/null +++ b/apps/web/src/components/schedule/ScheduleTable/constants.ts @@ -0,0 +1,29 @@ +import { Column } from '@web/components/common'; + +export const columns: Column[] = [ + { + id: 'date', + label: '날짜 변경', + width: '17rem', + }, + { + id: 'amPm', + label: '오전/오후', + width: '10.8rem', + }, + { + id: 'time', + label: '시', + width: '10.8rem', + }, + { + id: 'minute', + label: '분', + width: '10.8rem', + }, + { + id: 'action', + label: '순서 변경', + width: '52.5rem', + }, +]; diff --git a/apps/web/src/store/mutation/useCreateMorePostsMutation.ts b/apps/web/src/store/mutation/useCreateMorePostsMutation.ts index 7faa095b..bf39e872 100644 --- a/apps/web/src/store/mutation/useCreateMorePostsMutation.ts +++ b/apps/web/src/store/mutation/useCreateMorePostsMutation.ts @@ -30,14 +30,17 @@ export function useCreateMorePostsMutation({ } return POST( - `agents/${agentId}/post-groups/${postGroupId}/posts` + `v1/agents/${agentId}/post-groups/${postGroupId}/posts` ); }, onSuccess: () => { toast.success('게시글이 5개 추가됐어요!'); queryClient.invalidateQueries( - getAllPostsQueryOptions({ agentId, postGroupId }) + getAllPostsQueryOptions({ + agentId: Number(agentId), + postGroupId: Number(postGroupId), + }) ); }, onError: (error) => { diff --git a/apps/web/src/store/mutation/useCreatePostsMutation.ts b/apps/web/src/store/mutation/useCreatePostsMutation.ts index 9b9fee23..857e2b0b 100644 --- a/apps/web/src/store/mutation/useCreatePostsMutation.ts +++ b/apps/web/src/store/mutation/useCreatePostsMutation.ts @@ -38,7 +38,7 @@ export function useCreatePostsMutation({ agentId }: MutationCreatePostsType) { return useMutation({ mutationFn: (values: MutationCreatePostsRequest) => POST( - `agents/${agentId}/post-groups/posts`, + `v1/agents/${agentId}/post-groups/posts`, values ), onSuccess: (response) => { diff --git a/apps/web/src/store/mutation/useDeletePostGroupMutation.ts b/apps/web/src/store/mutation/useDeletePostGroupMutation.ts index 91360863..98a54810 100644 --- a/apps/web/src/store/mutation/useDeletePostGroupMutation.ts +++ b/apps/web/src/store/mutation/useDeletePostGroupMutation.ts @@ -2,7 +2,7 @@ import { DELETE } from '@web/shared/server'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useToast } from '@repo/ui/hooks'; import { IdParams, Post } from '@web/types'; -import { queryKeys } from '../constants'; +import { getAgentPostGroupsQueryOptions } from '../query/useGetAgentPostGroupsQuery'; export type MutationDeletePost = Pick; @@ -20,12 +20,12 @@ export function useDeletePostGroupMutation({ agentId }: MutationDeletePost) { return useMutation({ mutationFn: (postGroupId: Post['postGroupId']) => - DELETE(`agents/${agentId}/post-groups/${postGroupId}`), + DELETE(`v1/agents/${agentId}/post-groups/${postGroupId}`), onSuccess: () => { toast.success('주제가 삭제되었어요.'); - queryClient.invalidateQueries({ - queryKey: queryKeys.posts.postGroups(agentId), - }); + queryClient.invalidateQueries( + getAgentPostGroupsQueryOptions({ agentId: Number(agentId) }) + ); }, onError: (error) => { if (error instanceof Error) { diff --git a/apps/web/src/store/mutation/useDeletePostMutation.ts b/apps/web/src/store/mutation/useDeletePostMutation.ts index 53b7a646..bb2d4d2f 100644 --- a/apps/web/src/store/mutation/useDeletePostMutation.ts +++ b/apps/web/src/store/mutation/useDeletePostMutation.ts @@ -23,7 +23,7 @@ export function useDeletePostMutation({ return useMutation({ mutationFn: (postId: Post['id']) => - DELETE(`agents/${agentId}/post-groups/${postGroupId}/posts/${postId}`), + DELETE(`v1/agents/${agentId}/post-groups/${postGroupId}/posts/${postId}`), onSuccess: () => { toast.success('게시글이 삭제되었어요.'); queryClient.invalidateQueries( diff --git a/apps/web/src/store/mutation/useLogoutMutation.ts b/apps/web/src/store/mutation/useLogoutMutation.ts index dd56dca2..f0e9476c 100644 --- a/apps/web/src/store/mutation/useLogoutMutation.ts +++ b/apps/web/src/store/mutation/useLogoutMutation.ts @@ -13,7 +13,7 @@ export function useLogoutMutation() { const router = useRouter(); return useMutation({ - mutationFn: () => DELETE(`auth/logout`), + mutationFn: () => DELETE(`v1/auth/logout`), onSuccess: () => { toast.success('로그아웃이 완료되었어요.'); router.push(ROUTES.JOIN); diff --git a/apps/web/src/store/mutation/useUpdateMultiplePromptMutation.ts b/apps/web/src/store/mutation/useUpdateMultiplePromptMutation.ts index 7227ed7f..98d8d121 100644 --- a/apps/web/src/store/mutation/useUpdateMultiplePromptMutation.ts +++ b/apps/web/src/store/mutation/useUpdateMultiplePromptMutation.ts @@ -25,11 +25,17 @@ export function useUpdateMultiplePromptMutation({ return useMutation({ mutationFn: (data: UpdatePromptRequest) => - PATCH(`agents/${agentId}/post-groups/${postGroupId}/posts/prompt`, data), + PATCH( + `v1/agents/${agentId}/post-groups/${postGroupId}/posts/prompt`, + data + ), onSuccess: () => { toast.success('프롬프트가 적용되었어요!'); queryClient.invalidateQueries( - getAllPostsQueryOptions({ agentId, postGroupId }) + getAllPostsQueryOptions({ + agentId: Number(agentId), + postGroupId: Number(postGroupId), + }) ); }, onError: (error) => { diff --git a/apps/web/src/store/mutation/useUpdatePersonalSettingMutation.ts b/apps/web/src/store/mutation/useUpdatePersonalSettingMutation.ts index 6daea578..9f06722d 100644 --- a/apps/web/src/store/mutation/useUpdatePersonalSettingMutation.ts +++ b/apps/web/src/store/mutation/useUpdatePersonalSettingMutation.ts @@ -25,7 +25,7 @@ export function useUpdatePersonalSettingMutation({ return useMutation({ mutationFn: (data: UpdatePersonalRequest) => - PUT(`agents/${agentId}/personal-setting`, data), + PUT(`v1/agents/${agentId}/personal-setting`, data), onSuccess: () => { toast.success('저장되었어요.'); queryClient.invalidateQueries( diff --git a/apps/web/src/store/mutation/useUpdatePostMutation.ts b/apps/web/src/store/mutation/useUpdatePostMutation.ts index b088e367..f83950ea 100644 --- a/apps/web/src/store/mutation/useUpdatePostMutation.ts +++ b/apps/web/src/store/mutation/useUpdatePostMutation.ts @@ -27,7 +27,7 @@ export function useUpdatePostMutation({ return useMutation({ mutationFn: (values: MutationUpdatePostRequest) => PUT( - `agents/${agentId}/post-groups/${postGroupId}/posts/${postId}`, + `v1/agents/${agentId}/post-groups/${postGroupId}/posts/${postId}`, values ), onSuccess: () => { diff --git a/apps/web/src/store/mutation/useUpdatePostsMutation.ts b/apps/web/src/store/mutation/useUpdatePostsMutation.ts index 17bafe49..adccd35c 100644 --- a/apps/web/src/store/mutation/useUpdatePostsMutation.ts +++ b/apps/web/src/store/mutation/useUpdatePostsMutation.ts @@ -32,10 +32,13 @@ export function useUpdatePostsMutation({ return useMutation({ mutationFn: (data: UpdatePostsRequest) => - PUT(`agents/${agentId}/post-groups/${postGroupId}/posts`, data), + PUT(`v1/agents/${agentId}/post-groups/${postGroupId}/posts`, data), onSuccess: () => { queryClient.invalidateQueries( - getAllPostsQueryOptions({ agentId, postGroupId }) + getAllPostsQueryOptions({ + agentId: Number(agentId), + postGroupId: Number(postGroupId), + }) ); }, onError: (error) => { diff --git a/apps/web/src/store/mutation/useUpdateReservedPostsMutation.ts b/apps/web/src/store/mutation/useUpdateReservedPostsMutation.ts index e8211012..62a88100 100644 --- a/apps/web/src/store/mutation/useUpdateReservedPostsMutation.ts +++ b/apps/web/src/store/mutation/useUpdateReservedPostsMutation.ts @@ -24,7 +24,7 @@ export function useUpdateReservedPostsMutation(agentId: AgentId) { return useMutation({ mutationFn: (data: UpdateReservedPostsRequest) => - PUT(`agents/${agentId}/posts/upload-reserved`, data), + PUT(`v1/agents/${agentId}/posts/upload-reserved`, data), onError: (error) => { if (error instanceof Error) { toast.error(error.message); diff --git a/apps/web/src/store/mutation/useUpdateSinglePostPromptMutation.ts b/apps/web/src/store/mutation/useUpdateSinglePostPromptMutation.ts index 0fd855f4..caa13747 100644 --- a/apps/web/src/store/mutation/useUpdateSinglePostPromptMutation.ts +++ b/apps/web/src/store/mutation/useUpdateSinglePostPromptMutation.ts @@ -26,14 +26,17 @@ export function useUpdateSinglePostPromptMutation({ return useMutation({ mutationFn: (data: UpdateSinglePromptRequest) => { return PATCH( - `agents/${agentId}/post-groups/${postGroupId}/posts/${postId}/prompt`, + `v1/agents/${agentId}/post-groups/${postGroupId}/posts/${postId}/prompt`, data ); }, onSuccess: () => { toast.success('프롬프트가 적용되었어요!'); queryClient.invalidateQueries( - getAllPostsQueryOptions({ agentId, postGroupId }) + getAllPostsQueryOptions({ + agentId: Number(agentId), + postGroupId: Number(postGroupId), + }) ); }, onError: (error) => { diff --git a/apps/web/src/store/query/useGetAgentDetailQuery.ts b/apps/web/src/store/query/useGetAgentDetailQuery.ts index 6e9f40f4..7f0c15ff 100644 --- a/apps/web/src/store/query/useGetAgentDetailQuery.ts +++ b/apps/web/src/store/query/useGetAgentDetailQuery.ts @@ -31,7 +31,7 @@ export function getAgentDetailQueryOptions({ queryKey: queryKeys.agents.detail(agentId), queryFn: async () => { const response = await GET( - `agents/${agentId}`, + `v1/agents/${agentId}`, undefined, tokens ); diff --git a/apps/web/src/store/query/useGetAgentPostGroupsQuery.ts b/apps/web/src/store/query/useGetAgentPostGroupsQuery.ts index 7903ec03..6e1df0e5 100644 --- a/apps/web/src/store/query/useGetAgentPostGroupsQuery.ts +++ b/apps/web/src/store/query/useGetAgentPostGroupsQuery.ts @@ -24,7 +24,7 @@ export function getAgentPostGroupsQueryOptions({ queryKey: queryKeys.posts.postGroups(agentId), queryFn: async () => { const response = await GET( - `agents/${agentId}/post-groups`, + `v1/agents/${agentId}/post-groups`, undefined, tokens ); diff --git a/apps/web/src/store/query/useGetAgentQuery.ts b/apps/web/src/store/query/useGetAgentQuery.ts index 21374b43..1f2af85c 100644 --- a/apps/web/src/store/query/useGetAgentQuery.ts +++ b/apps/web/src/store/query/useGetAgentQuery.ts @@ -15,7 +15,12 @@ export function getAgentQueryOptions(tokens?: Tokens) { return queryOptions({ queryKey: queryKeys.agents.agents, queryFn: async () => { - const response = await GET(`agents`, undefined, tokens); + const response = await GET( + `v1/agents`, + undefined, + tokens + ); + return response.data; }, staleTime: STALE_TIME, diff --git a/apps/web/src/store/query/useGetAllPostsQuery.ts b/apps/web/src/store/query/useGetAllPostsQuery.ts index bf3f1ee8..90d29265 100644 --- a/apps/web/src/store/query/useGetAllPostsQuery.ts +++ b/apps/web/src/store/query/useGetAllPostsQuery.ts @@ -30,7 +30,7 @@ export function getAllPostsQueryOptions({ queryKey: queryKeys.posts.all(Number(agentId), Number(postGroupId)), queryFn: () => GET( - `agents/${agentId}/post-groups/${postGroupId}/posts`, + `v1/agents/${agentId}/post-groups/${postGroupId}/posts`, undefined, tokens ), diff --git a/apps/web/src/store/query/useGetPostQuery.ts b/apps/web/src/store/query/useGetPostQuery.ts index 4fccd6ee..3b91bda8 100644 --- a/apps/web/src/store/query/useGetPostQuery.ts +++ b/apps/web/src/store/query/useGetPostQuery.ts @@ -29,7 +29,7 @@ export function getPostQueryOptions({ queryKey: queryKeys.posts.detail(agentId, postGroupId, postId), queryFn: () => GET( - `agents/${agentId}/post-groups/${postGroupId}/posts/${postId}`, + `v1/agents/${agentId}/post-groups/${postGroupId}/posts/${postId}`, undefined, tokens ), diff --git a/apps/web/src/store/query/useGetTopicQuery.ts b/apps/web/src/store/query/useGetTopicQuery.ts index 0d881e82..002a5149 100644 --- a/apps/web/src/store/query/useGetTopicQuery.ts +++ b/apps/web/src/store/query/useGetTopicQuery.ts @@ -29,7 +29,7 @@ export function getTopicQueryOptions({ queryKey: queryKeys.topics.detail(agentId, postGroupId), queryFn: () => GET( - `agents/${agentId}/post-groups/${postGroupId}/topic`, + `v1/agents/${agentId}/post-groups/${postGroupId}/topic`, undefined, tokens ), diff --git a/apps/web/src/store/query/useGetUserQuery.ts b/apps/web/src/store/query/useGetUserQuery.ts index 89da5115..b00d97cf 100644 --- a/apps/web/src/store/query/useGetUserQuery.ts +++ b/apps/web/src/store/query/useGetUserQuery.ts @@ -19,7 +19,7 @@ export type GetUserParams = { export function getUserQueryOptions(tokens?: Tokens) { return queryOptions({ queryKey: queryKeys.user, - queryFn: () => GET(`users`, undefined, tokens), + queryFn: () => GET(`v1/users`, undefined, tokens), staleTime: STALE_TIME, gcTime: GC_TIME, }); diff --git a/apps/web/src/store/query/useGetXLogin.ts b/apps/web/src/store/query/useGetXLogin.ts index 2065cb95..d564fcf6 100644 --- a/apps/web/src/store/query/useGetXLogin.ts +++ b/apps/web/src/store/query/useGetXLogin.ts @@ -16,7 +16,7 @@ export function getXLoginQueryOptions() { return queryOptions({ queryKey: queryKeys.x, queryFn: () => - GET(`twitter/login`, undefined, undefined), + GET(`v1/twitter/login`, undefined, undefined), staleTime: STALE_TIME, gcTime: GC_TIME, enabled: false, diff --git a/apps/web/src/store/query/useNewsCategoriesQuery.ts b/apps/web/src/store/query/useNewsCategoriesQuery.ts index 1092c9a0..66faa45c 100644 --- a/apps/web/src/store/query/useNewsCategoriesQuery.ts +++ b/apps/web/src/store/query/useNewsCategoriesQuery.ts @@ -1,7 +1,12 @@ -import { queryOptions, useSuspenseQuery } from '@tanstack/react-query'; +import { + queryOptions, + useQueryClient, + useSuspenseQuery, +} from '@tanstack/react-query'; import { GET } from '@web/shared/server/fetch'; import { Tokens } from '@web/shared/server/types'; import { queryKeys } from '../constants'; +import { useEffect } from 'react'; export interface NewsCategory { /** @@ -20,7 +25,7 @@ export function newsCategoriesQueryOptions(tokens?: Tokens) { return queryOptions({ queryKey: queryKeys.news.categories, queryFn: () => - GET(`news-categories`, undefined, tokens), + GET(`v1/news-categories`, undefined, tokens), // NOTE: 항상 fresh 상태로 유지 staleTime: Infinity, gcTime: Infinity, @@ -30,3 +35,11 @@ export function newsCategoriesQueryOptions(tokens?: Tokens) { export function useNewsCategoriesQuery() { return useSuspenseQuery(newsCategoriesQueryOptions()); } + +export function useClientSidePrefetchNewsCategories() { + const queryClient = useQueryClient(); + + useEffect(() => { + queryClient.prefetchQuery(newsCategoriesQueryOptions()); + }, [queryClient]); +} diff --git a/apps/web/src/store/query/usePostHistoryQuery.ts b/apps/web/src/store/query/usePostHistoryQuery.ts index 6b4872c9..9932e3eb 100644 --- a/apps/web/src/store/query/usePostHistoryQuery.ts +++ b/apps/web/src/store/query/usePostHistoryQuery.ts @@ -30,7 +30,7 @@ export function PostHistoryQueryQueryOptions({ queryKey: queryKeys.postHistory.detail(postId), queryFn: () => GET( - `agents/${agentId}/post-groups/${postGroupId}/posts/${postId}/prompt-histories`, + `v1/agents/${agentId}/post-groups/${postGroupId}/posts/${postId}/prompt-histories`, undefined, tokens ), diff --git a/apps/web/src/types/post.ts b/apps/web/src/types/post.ts index 0c56f311..aea81735 100644 --- a/apps/web/src/types/post.ts +++ b/apps/web/src/types/post.ts @@ -11,6 +11,7 @@ export const POST_STATUS = { EDITING: 'EDITING', READY_TO_UPLOAD: 'READY_TO_UPLOAD', UPLOAD_RESERVED: 'UPLOAD_RESERVED', + UPLOAD_CONFIRMED: 'UPLOAD_CONFIRMED', UPLOADED: 'UPLOADED', UPLOAD_FAILED: 'UPLOAD_FAILED', } as const; diff --git a/apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/schedule/utils/getCurrentDateKo.ts b/apps/web/src/utils/getCurrentDateKo.ts similarity index 100% rename from apps/web/src/app/(prompt)/edit/[agentId]/[postGroupId]/schedule/utils/getCurrentDateKo.ts rename to apps/web/src/utils/getCurrentDateKo.ts diff --git a/apps/web/src/utils/index.ts b/apps/web/src/utils/index.ts index 7ad6b6f1..941cbf24 100644 --- a/apps/web/src/utils/index.ts +++ b/apps/web/src/utils/index.ts @@ -5,3 +5,4 @@ export { getFormattedDatetime } from './getFormattedDatetime'; export { validateScheduleDate } from './validateScheduleDate'; export { parseTime } from './parseTime'; export { getFormattedHourByAMPM } from './getFormattedHourByAMPM'; +export { getCurrentDateKo } from './getCurrentDateKo'; diff --git a/apps/web/src/utils/parseTime.ts b/apps/web/src/utils/parseTime.ts index 923188b4..8419765c 100644 --- a/apps/web/src/utils/parseTime.ts +++ b/apps/web/src/utils/parseTime.ts @@ -1,16 +1,20 @@ /** * @param uploadTime - 변환할 날짜와 시간 (Date 객체 또는 ISO 8601 문자열) - * @returns 예시: { date: '2025-02-14', hour: '15', minute: '30' } + * @returns 예시: { date: '2025-02-14', hour: '03', minute: '30', amPm: '오후' } */ -export function parseTime(uploadTime?: string) { - if (!uploadTime) { +export function parseTime(time?: string) { + if (!time) { return null; } - const dateTime = new Date(uploadTime); + const dateTime = new Date(time); + const localHour = dateTime.getHours(); + const hour12 = localHour % 12 === 0 ? 12 : localHour % 12; + const amPm = localHour < 12 ? '오전' : '오후'; return { date: dateTime.toISOString().split('T')[0], - hour: dateTime.getUTCHours().toString().padStart(2, '0'), - minute: dateTime.getUTCMinutes().toString().padStart(2, '0'), + hour: hour12.toString().padStart(2, '0'), + minute: dateTime.getMinutes().toString().padStart(2, '0'), + amPm, }; } diff --git a/packages/ui/src/components/Modal/Modal.css.ts b/packages/ui/src/components/Modal/Modal.css.ts index 634a1fea..9f6b6ce3 100644 --- a/packages/ui/src/components/Modal/Modal.css.ts +++ b/packages/ui/src/components/Modal/Modal.css.ts @@ -8,7 +8,7 @@ export const overlay = style({ right: 0, bottom: 0, backgroundColor: 'rgba(0, 0, 0, 0.5)', - zIndex: 0, + zIndex: 10000, }); export const container = style({ @@ -18,7 +18,7 @@ export const container = style({ backgroundColor: vars.colors.grey, borderRadius: '2.4rem', padding: `5.2rem ${vars.space[24]} ${vars.space[24]} ${vars.space[24]}`, - zIndex: 1, + zIndex: 10001, width: '56rem', display: 'flex', flexDirection: 'column', diff --git a/packages/ui/src/components/Toast/Toast.css.ts b/packages/ui/src/components/Toast/Toast.css.ts index 1c55267c..7780d9ed 100644 --- a/packages/ui/src/components/Toast/Toast.css.ts +++ b/packages/ui/src/components/Toast/Toast.css.ts @@ -10,6 +10,7 @@ export const container = recipe({ borderRadius: 100, backgroundColor: vars.colors.grey700, color: vars.colors.grey, + zIndex: 10000, }, variants: { toastPosition: {