diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 3e8b2294d..d93b6a4c2 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -33,6 +33,11 @@ version-resolver: - '๐Ÿš˜ Patch' default: patch +sort-by: merged_at +sort-direction: ascending +filter-by-commitish: false + + template: | ## โœจ Changes $CHANGES diff --git a/.gitignore b/.gitignore index 22bb86ce8..34f350dae 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,7 @@ storybook-static # Changesets .changeset/pre.json + +# cursor +.cursorrules +.cursorignore \ No newline at end of file diff --git a/apps/client/package.json b/apps/client/package.json index fd5c92abe..7496b6083 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -11,36 +11,39 @@ }, "dependencies": { "@bds/ui": "workspace:*", + "@hookform/resolvers": "^5.2.1", "@tanstack/react-query": "^5.83.0", "@tanstack/react-query-devtools": "^5.83.0", "@toss/ky": "^1.2.1", + "@types/react": "catalog:react-core", + "@types/react-dom": "catalog:react-core", "lottie-react": "^2.4.1", "react": "catalog:react-core", "react-dom": "catalog:react-core", "react-error-boundary": "^6.0.0", + "react-hook-form": "^7.62.0", "react-router": "^7.6.3", "react-router-dom": "^7.6.3", - "swiper": "^11.2.10", - "@types/react": "catalog:react-core", - "@types/react-dom": "catalog:react-core" + "react-textarea-autosize": "^8.5.9", + "zod": "^4.0.17" }, "devDependencies": { "@bofit/eslint": "workspace:*", "@bofit/typescript": "workspace:*", "@pivanov/vite-plugin-svg-sprite": "^3.0.0", + "@types/node": "catalog:typescript-core", "@typescript-eslint/eslint-plugin": "^5.59.0", "@typescript-eslint/parser": "^5.59.0", "@vanilla-extract/css": "catalog:vanilla-extract-core", "@vanilla-extract/recipes": "catalog:vanilla-extract-core", "@vanilla-extract/sprinkles": "catalog:vanilla-extract-utils", "@vanilla-extract/vite-plugin": "catalog:vanilla-extract-utils", - "vite": "catalog:vite-core", "@vitejs/plugin-react": "catalog:vite-plugins", "eslint": "^8.44.0", "globals": "^16.2.0", "openapi-typescript": "^7.8.0", "typescript": "catalog:typescript-core", - "@types/node": "catalog:typescript-core", + "vite": "catalog:vite-core", "vite-tsconfig-paths": "^5.1.4" } } diff --git a/apps/client/public/3d_brain.webp b/apps/client/public/3d_brain.webp new file mode 100644 index 000000000..35840e8e0 Binary files /dev/null and b/apps/client/public/3d_brain.webp differ diff --git a/apps/client/public/3d_cancer.webp b/apps/client/public/3d_cancer.webp new file mode 100644 index 000000000..c23f6ff39 Binary files /dev/null and b/apps/client/public/3d_cancer.webp differ diff --git a/apps/client/public/3d_die.webp b/apps/client/public/3d_die.webp new file mode 100644 index 000000000..fce587796 Binary files /dev/null and b/apps/client/public/3d_die.webp differ diff --git a/apps/client/public/3d_disability.webp b/apps/client/public/3d_disability.webp new file mode 100644 index 000000000..a1d6f31df Binary files /dev/null and b/apps/client/public/3d_disability.webp differ diff --git a/apps/client/public/3d_heart.webp b/apps/client/public/3d_heart.webp new file mode 100644 index 000000000..9aad4971d Binary files /dev/null and b/apps/client/public/3d_heart.webp differ diff --git a/apps/client/public/3d_hospital.webp b/apps/client/public/3d_hospital.webp new file mode 100644 index 000000000..be735dce9 Binary files /dev/null and b/apps/client/public/3d_hospital.webp differ diff --git a/apps/client/public/3d_icon_logo.webp b/apps/client/public/3d_icon_logo.webp new file mode 100644 index 000000000..4b0af53dd Binary files /dev/null and b/apps/client/public/3d_icon_logo.webp differ diff --git a/apps/client/public/3d_surgery.webp b/apps/client/public/3d_surgery.webp new file mode 100644 index 000000000..de008e695 Binary files /dev/null and b/apps/client/public/3d_surgery.webp differ diff --git a/apps/client/public/glass_icon_bulb.svg b/apps/client/public/glass_icon_bulb.svg deleted file mode 100644 index 8e8b3abf9..000000000 --- a/apps/client/public/glass_icon_bulb.svg +++ /dev/null @@ -1,22 +0,0 @@ - - - -
- - -
- - - - - - - - - - - - - - -
diff --git a/apps/client/public/glass_icon_bulb.webp b/apps/client/public/glass_icon_bulb.webp new file mode 100644 index 000000000..38599f7d1 Binary files /dev/null and b/apps/client/public/glass_icon_bulb.webp differ diff --git a/apps/client/public/glass_icon_chat.webp b/apps/client/public/glass_icon_chat.webp index 70d92f610..35499e88a 100644 Binary files a/apps/client/public/glass_icon_chat.webp and b/apps/client/public/glass_icon_chat.webp differ diff --git a/apps/client/public/glass_icon_chat_dark.webp b/apps/client/public/glass_icon_chat_dark.webp new file mode 100644 index 000000000..b2dd1c490 Binary files /dev/null and b/apps/client/public/glass_icon_chat_dark.webp differ diff --git a/apps/client/public/logo_3d.webp b/apps/client/public/logo_3d.webp new file mode 100644 index 000000000..a7265615e Binary files /dev/null and b/apps/client/public/logo_3d.webp differ diff --git a/apps/client/public/logotype_3d.webp b/apps/client/public/logotype_3d.webp new file mode 100644 index 000000000..49050d724 Binary files /dev/null and b/apps/client/public/logotype_3d.webp differ diff --git a/apps/client/src/pages/community/community-detail.tsx b/apps/client/src/pages/community/community-detail.tsx index 94393a2d8..34f719cc7 100644 --- a/apps/client/src/pages/community/community-detail.tsx +++ b/apps/client/src/pages/community/community-detail.tsx @@ -4,6 +4,7 @@ import { Navigation } from '@bds/ui'; import { Icon } from '@bds/ui/icons'; import DetailSection from '@widgets/community/components/detail-section/detail-section'; +import { InputModeContextProvider } from '@widgets/community/context/input-mode-context'; import { useNavigateTo } from '@shared/hooks/use-navigate-to'; import { routePath } from '@shared/router/path'; @@ -16,17 +17,19 @@ const CommunityDetail = () => { } return ( - <> + } + searchIcon={} onClickLeft={useNavigateTo(-1)} rightIcon={} onClickRight={useNavigateTo(routePath.HOME)} + onClickSearch={useNavigateTo(routePath.COMMUNITY_SEARCH)} /> - + ); }; diff --git a/apps/client/src/pages/community/community-edit/community-edit.css.ts b/apps/client/src/pages/community/community-edit/community-edit.css.ts index dfd89a396..5e2a99af3 100644 --- a/apps/client/src/pages/community/community-edit/community-edit.css.ts +++ b/apps/client/src/pages/community/community-edit/community-edit.css.ts @@ -1,9 +1,10 @@ import { style } from '@vanilla-extract/css'; export const container = style({ + overflowY: 'auto', display: 'flex', + height: '100dvh', flexDirection: 'column', - height: '100vh', gap: '3.6rem', }); @@ -26,3 +27,28 @@ export const postContent = style({ flexDirection: 'column', gap: '1.2rem', }); + +export const postTitle = style({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', +}); + +export const imageContainer = style({ + display: 'flex', + flexDirection: 'column', + gap: '1.2rem', + padding: '1.2rem 0 5.5rem 0', +}); + +export const imageItem = style({ + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-end', +}); + +export const postImage = style({ + width: '100%', + objectFit: 'cover', + borderRadius: '12px', +}); diff --git a/apps/client/src/pages/community/community-edit/community-edit.tsx b/apps/client/src/pages/community/community-edit/community-edit.tsx index 8ade834d9..bd5a0a2ec 100644 --- a/apps/client/src/pages/community/community-edit/community-edit.tsx +++ b/apps/client/src/pages/community/community-edit/community-edit.tsx @@ -1,15 +1,30 @@ -import { useEffect, useState } from 'react'; -import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { useLocation, useParams } from 'react-router-dom'; -import { useNavigate } from 'react-router-dom'; +import { ChangeEvent, useState } from 'react'; +import { + useMutation, + useQueryClient, + useSuspenseQuery, +} from '@tanstack/react-query'; +import { useNavigate, useParams } from 'react-router-dom'; import { Input, Navigation, TextButton, Title } from '@bds/ui'; import { Icon } from '@bds/ui/icons'; +import CommunityImageUploader from '@widgets/community/components/community-image-uploader/community-image-uploader'; import CommunityLine from '@widgets/community/components/community-line/community-line'; +import FilterDropDown from '@widgets/community/components/filter-dropdown/filter-dropdown'; +import { categoryOptions } from '@widgets/community/configs/category-config'; import { PLACEHOLDER } from '@widgets/community/constant/input-placeholder'; +import { CategoryType } from '@widgets/community/types/category-type'; +import { isValidImage } from '@widgets/community/utils/type-guard'; -import { COMMUNITY_MUTATION_OPTIONS } from '@shared/api/domain/community/queries'; +import { + COMMUNITY_MUTATION_OPTIONS, + COMMUNITY_QUERY_OPTIONS, +} from '@shared/api/domain/community/queries'; +import { + MUTATION_QUERY_OPTIONS, + uploadImageToS3, +} from '@shared/api/domain/queries'; import { COMMUNITY_QUERY_KEY } from '@shared/api/keys/query-key'; import { LIMIT_LONG_TEXT, @@ -17,33 +32,24 @@ import { } from '@shared/constants/text-limits'; import { useLimitedInput } from '@shared/hooks/use-limited-input'; import { routePath } from '@shared/router/path'; +import { extractS3Urls } from '@shared/utils/utils'; import * as styles from './community-edit.css'; -const COMMUNITY_CONTENT = { - TITLE: { - HEADER: '์ œ๋ชฉ', - BODY: '๋‚ด์šฉ', - }, - BUTTON: '์ˆ˜์ •', -}; - const CommunityEdit = () => { const navigate = useNavigate(); - const [isDisabled, setIsDisabled] = useState(true); const queryClient = useQueryClient(); const { postId } = useParams<{ postId: string }>(); - const location = useLocation(); - const state = location.state as { title: string; content: string }; - const [title, setTitle] = useState(state.title); - const [content, setContent] = useState(state.content); - const { isErrorState } = useLimitedInput(LIMIT_SHORT_TEXT, title.length); if (!postId) { throw new Error('๊ฒŒ์‹œ๊ธ€ Id๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.'); } - const { mutate } = useMutation({ + const { data: feedDetailData } = useSuspenseQuery( + COMMUNITY_QUERY_OPTIONS.FEED_DETAIL(postId), + ); + + const { mutate, isPending } = useMutation({ ...COMMUNITY_MUTATION_OPTIONS.PUT_FEED(postId), onSuccess: () => { queryClient.invalidateQueries({ @@ -53,38 +59,119 @@ const CommunityEdit = () => { }, }); - const handlePutFeed = () => { + const { mutate: postImageUploadMutate } = useMutation({ + ...MUTATION_QUERY_OPTIONS.POST_IMAGE(), + }); + + if (!feedDetailData) { + throw new Error( + '๊ธ€ ์ˆ˜์ • ํŽ˜์ด์ง€์—์„œ ์›๋ณธ ํ”ผ๋“œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.', + ); + } + + const [title, setTitle] = useState(feedDetailData?.title || ''); + const [content, setContent] = useState(feedDetailData?.content || ''); + const [category, setCategory] = useState( + feedDetailData?.category?.category || '', + ); + const [newImages, setNewImages] = useState< + { file: File; previewUrl: string }[] + >([]); + const [deletedImageIds, setDeletedImageIds] = useState([]); + const [updatedImages, setUpdatedImages] = useState< + { id?: number; imageUrl: string; sequence: number }[] + >( + () => + feedDetailData.imageUrl?.filter(isValidImage).map((img, index) => ({ + id: img.imageId, + imageUrl: img.imageUrl, + sequence: index + 1, + })) ?? [], + ); + const { isErrorState } = useLimitedInput(LIMIT_SHORT_TEXT, title.length); + + const handleUploadNewImages = async () => { + if (newImages.length === 0) { + return []; + } + + const data = await new Promise<{ presignedUrls: string[] }>( + (resolve, reject) => + postImageUploadMutate( + newImages.map((item) => item.file.type), + { onSuccess: resolve, onError: reject }, + ), + ); + + await Promise.all( + data.presignedUrls.map((url, idx) => + uploadImageToS3(url, newImages[idx].file), + ), + ); + + const uploadedUrls = extractS3Urls(data.presignedUrls).map((url, idx) => ({ + imageUrl: url, + sequence: updatedImages.length + idx, + })); + + setUpdatedImages((prev) => [...prev, ...uploadedUrls]); + setNewImages([]); + + return uploadedUrls; + }; + + const handlePutFeed = async () => { + const newUploadedImages = await handleUploadNewImages(); + mutate({ body: { - title: title, - content: content, + newTitle: title, + newContent: content, + newCategory: category, + deleteImageIds: deletedImageIds, + updatedImages: [...updatedImages, ...newUploadedImages], }, }); }; - useEffect(() => { - const isTitleValid = title.trim().length > 0; - const isContentValid = content.trim().length > 0; - - setIsDisabled(!(isTitleValid && isContentValid)); - }, [title, content]); - const handleGoBack = () => { navigate(-1); }; - const handleTitleChange = (e: React.ChangeEvent) => { - if (e.target.value.length <= 30) { + const handleTitleChange = (e: ChangeEvent) => { + if (e.target.value.length <= LIMIT_SHORT_TEXT) { setTitle(e.target.value); } }; - const handleContentChange = (e: React.ChangeEvent) => { + const handleContentChange = (e: ChangeEvent) => { if (e.target.value.length <= LIMIT_LONG_TEXT) { setContent(e.target.value); } }; + const handleCategory = (option: CategoryType) => { + setCategory(option.value); + }; + + const handleImageChange = (files: FileList) => { + const newImages = Array.from(files).map((file) => ({ + file, + previewUrl: URL.createObjectURL(file), + })); + setNewImages((prev) => [...prev, ...newImages]); + }; + + const handleRemoveNewImage = (urlToRemove: string) => { + setNewImages((prev) => + prev.filter((item) => item.previewUrl !== urlToRemove), + ); + }; + + const handleRemoveOriginImage = (imageId: number) => { + setDeletedImageIds((prev) => [...prev, imageId]); + }; + return (
{ } rightIcon={ { - (handlePutFeed(), handleGoBack()); + handlePutFeed(); }} > - {COMMUNITY_CONTENT.BUTTON} + ์™„๋ฃŒ } - isTextButton={true} + isTextButton />
- {COMMUNITY_CONTENT.TITLE.HEADER} +
+ ์ œ๋ชฉ + opt.value === category)?.label + } + rightIcon={} + isIconRotate + > + {categoryOptions.map((option) => ( + handleCategory(option)} + > + {option.label} + + ))} + +
- {COMMUNITY_CONTENT.TITLE.BODY} + ๋‚ด์šฉ + {(feedDetailData.imageUrl?.some(isValidImage) || + newImages.length > 0) && ( +
+ {feedDetailData.imageUrl + ?.filter(isValidImage) + .filter((img) => !deletedImageIds.includes(img.imageId)) + .map((orgImage) => ( +
+ uploaded + handleRemoveOriginImage(orgImage.imageId)} + > + ์‚ญ์ œ + +
+ ))} + {newImages.map((image) => ( +
+ preview + handleRemoveNewImage(image.previewUrl)} + > + ์‚ญ์ œ + +
+ ))} +
+ )}
+
); }; diff --git a/apps/client/src/pages/community/community-page.tsx b/apps/client/src/pages/community/community-page.tsx index 09aa5cd0d..ed27c9feb 100644 --- a/apps/client/src/pages/community/community-page.tsx +++ b/apps/client/src/pages/community/community-page.tsx @@ -11,7 +11,9 @@ const CommunityPage = () => { <> } + searchIcon={} onClickRight={useNavigateTo(routePath.HOME)} + onClickSearch={useNavigateTo(routePath.COMMUNITY_SEARCH)} title="์ปค๋ฎค๋‹ˆํ‹ฐ" /> diff --git a/apps/client/src/pages/community/community-search/community-search.tsx b/apps/client/src/pages/community/community-search/community-search.tsx new file mode 100644 index 000000000..e863de21a --- /dev/null +++ b/apps/client/src/pages/community/community-search/community-search.tsx @@ -0,0 +1,21 @@ +import { Navigation } from '@bds/ui'; +import { Icon } from '@bds/ui/icons'; + +import Search from '@widgets/community/components/search/search'; + +import { useNavigateTo } from '@shared/hooks/use-navigate-to'; + +const CommunitySearch = () => { + return ( + <> + } + onClickLeft={useNavigateTo(-1)} + title="์ปค๋ฎค๋‹ˆํ‹ฐ ๊ฒ€์ƒ‰" + /> + + + ); +}; + +export default CommunitySearch; diff --git a/apps/client/src/pages/community/community-write/community-write.css.ts b/apps/client/src/pages/community/community-write/community-write.css.ts index dfd89a396..5e2a99af3 100644 --- a/apps/client/src/pages/community/community-write/community-write.css.ts +++ b/apps/client/src/pages/community/community-write/community-write.css.ts @@ -1,9 +1,10 @@ import { style } from '@vanilla-extract/css'; export const container = style({ + overflowY: 'auto', display: 'flex', + height: '100dvh', flexDirection: 'column', - height: '100vh', gap: '3.6rem', }); @@ -26,3 +27,28 @@ export const postContent = style({ flexDirection: 'column', gap: '1.2rem', }); + +export const postTitle = style({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', +}); + +export const imageContainer = style({ + display: 'flex', + flexDirection: 'column', + gap: '1.2rem', + padding: '1.2rem 0 5.5rem 0', +}); + +export const imageItem = style({ + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-end', +}); + +export const postImage = style({ + width: '100%', + objectFit: 'cover', + borderRadius: '12px', +}); diff --git a/apps/client/src/pages/community/community-write/community-write.tsx b/apps/client/src/pages/community/community-write/community-write.tsx index ee47d2e47..88319306f 100644 --- a/apps/client/src/pages/community/community-write/community-write.tsx +++ b/apps/client/src/pages/community/community-write/community-write.tsx @@ -1,14 +1,22 @@ -import { useEffect, useState } from 'react'; +import { ChangeEvent, useState } from 'react'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useNavigate } from 'react-router-dom'; import { Input, Navigation, TextButton, Title } from '@bds/ui'; import { Icon } from '@bds/ui/icons'; +import CommunityImageUploader from '@widgets/community/components/community-image-uploader/community-image-uploader'; import CommunityLine from '@widgets/community/components/community-line/community-line'; +import FilterDropDown from '@widgets/community/components/filter-dropdown/filter-dropdown'; +import { categoryOptions } from '@widgets/community/configs/category-config'; import { PLACEHOLDER } from '@widgets/community/constant/input-placeholder'; +import { CategoryType } from '@widgets/community/types/category-type'; import { COMMUNITY_MUTATION_OPTIONS } from '@shared/api/domain/community/queries'; +import { + MUTATION_QUERY_OPTIONS, + uploadImageToS3, +} from '@shared/api/domain/queries'; import { COMMUNITY_QUERY_KEY } from '@shared/api/keys/query-key'; import { LIMIT_LONG_TEXT, @@ -16,25 +24,23 @@ import { } from '@shared/constants/text-limits'; import { useLimitedInput } from '@shared/hooks/use-limited-input'; import { routePath } from '@shared/router/path'; +import { extractS3Urls } from '@shared/utils/utils'; import * as styles from './community-write.css'; -const COMMUNITY_CONTENT = { - TITLE: { - HEADER: '์ œ๋ชฉ', - BODY: '๋‚ด์šฉ', - }, - BUTTON: '์—…๋กœ๋“œ', -}; - const CommunityWrite = () => { const navigate = useNavigate(); + const queryClient = useQueryClient(); const [title, setTitle] = useState(''); const [content, setContent] = useState(''); - const [isDisabled, setIsDisabled] = useState(true); - const queryClient = useQueryClient(); + const [category, setCategory] = useState(null); + const [uploadedImages, setUploadedImages] = useState< + { file: File; previewUrl: string }[] + >([]); + const { isErrorState } = useLimitedInput(LIMIT_SHORT_TEXT, title.length); - const { mutate } = useMutation({ + + const { mutate: postFeedMutate, isPending } = useMutation({ ...COMMUNITY_MUTATION_OPTIONS.POST_FEED(), onSuccess: () => { queryClient.invalidateQueries({ @@ -44,36 +50,87 @@ const CommunityWrite = () => { }, }); - const handlePostFeed = () => { - mutate({ - title: title, - content: content, - }); - }; - - useEffect(() => { - const isTitleValid = title.trim().length > 0; - const isContentValid = content.trim().length > 0; + const { mutate: postImageUploadMutate } = useMutation({ + ...MUTATION_QUERY_OPTIONS.POST_IMAGE(), + }); - setIsDisabled(!(isTitleValid && isContentValid)); - }, [title, content]); + const isDisabled = + !(title.trim() && content.trim() && category?.value) || isPending; const handleGoBack = () => { navigate(-1); }; - const handleTitleChange = (e: React.ChangeEvent) => { + const handleTitleChange = (e: ChangeEvent) => { if (e.target.value.length <= LIMIT_SHORT_TEXT) { setTitle(e.target.value); } }; - const handleContentChange = (e: React.ChangeEvent) => { + const handleContentChange = (e: ChangeEvent) => { if (e.target.value.length <= LIMIT_LONG_TEXT) { setContent(e.target.value); } }; + const handleCategory = (option: CategoryType) => { + setCategory(option); + }; + + const handleImageChange = (files: FileList) => { + const newImages = Array.from(files).map((file) => ({ + file, + previewUrl: URL.createObjectURL(file), + })); + setUploadedImages((prev) => [...prev, ...newImages]); + }; + + const handleRemoveImage = (urlToRemove: string) => { + setUploadedImages((prev) => + prev.filter((item) => item.previewUrl !== urlToRemove), + ); + }; + + const uploadAllImages = async () => { + if (uploadedImages.length === 0) { + return []; + } + + const data = await new Promise<{ presignedUrls: string[] }>( + (resolve, reject) => + postImageUploadMutate( + uploadedImages.map((item) => item.file.type), + { + onSuccess: resolve, + onError: reject, + }, + ), + ); + await Promise.all( + data.presignedUrls.map((url, idx) => + uploadImageToS3(url, uploadedImages[idx].file), + ), + ); + + return extractS3Urls(data.presignedUrls); + }; + + const submitFeed = async (imageUrls: string[]) => { + postFeedMutate({ + title, + content, + category: category!.value, + imageUrls, + }); + }; + + const handlePostFeed = async () => { + if (isDisabled) { + return null; + } + return submitFeed(await uploadAllImages()); + }; + return (
{ leftIcon={} onClickLeft={handleGoBack} rightIcon={ - - {COMMUNITY_CONTENT.BUTTON} + + ์—…๋กœ๋“œ } onClickRight={handlePostFeed} - isTextButton={true} + isTextButton />
- {COMMUNITY_CONTENT.TITLE.HEADER} +
+ ์ œ๋ชฉ + } + isIconRotate + > + {categoryOptions.map((option) => ( + handleCategory(option)} + > + {option.label} + + ))} + +
- {COMMUNITY_CONTENT.TITLE.BODY} + ๋‚ด์šฉ + {uploadedImages.length > 0 && ( +
+ {uploadedImages.map((image) => ( +
+ + handleRemoveImage(image.previewUrl)} + > + ์‚ญ์ œ + +
+ ))} +
+ )}
+
); }; diff --git a/apps/client/src/pages/home/home-page.tsx b/apps/client/src/pages/home/home-page.tsx index cbb547424..830c17b3d 100644 --- a/apps/client/src/pages/home/home-page.tsx +++ b/apps/client/src/pages/home/home-page.tsx @@ -13,8 +13,6 @@ import { routePath } from '@shared/router/path.ts'; import * as styles from './home-page.css.ts'; -import 'swiper/swiper-bundle.css'; - const HomePage = () => { const navigate = useNavigate(); diff --git a/apps/client/src/pages/my/my-page.tsx b/apps/client/src/pages/my/my-page.tsx index 6f9ccbcd2..fc981093e 100644 --- a/apps/client/src/pages/my/my-page.tsx +++ b/apps/client/src/pages/my/my-page.tsx @@ -4,19 +4,16 @@ import { useNavigate } from 'react-router-dom'; import { Navigation } from '@bds/ui'; import { Icon } from '@bds/ui/icons'; -import Body from '@widgets/mypage/body'; +import Body from '@widgets/mypage/components/body/body'; import { USER_QUERY_OPTIONS } from '@shared/api/domain/mypage/queries'; +import { useNavigateTo } from '@shared/hooks/use-navigate-to'; import { routePath } from '@shared/router/path'; const MyPage = () => { const { data: queryData } = useSuspenseQuery(USER_QUERY_OPTIONS.PROFILE()); const userData = queryData?.data; - const targetRoute = userData?.isRecommendInsurance - ? routePath.REPORT - : routePath.HOME; - const navigate = useNavigate(); const handleNavigate = (route: string) => { @@ -26,8 +23,10 @@ const MyPage = () => { return ( <> } + searchIcon={} + onClickSearch={useNavigateTo(routePath.COMMUNITY_SEARCH)} onClickRight={() => handleNavigate(routePath.HOME)} backgroundColor="primary" textColor="white" @@ -37,8 +36,6 @@ const MyPage = () => { handleNavigate(targetRoute)} /> ); diff --git a/apps/client/src/pages/onboarding/onboarding-page.css.ts b/apps/client/src/pages/onboarding/onboarding-page.css.ts deleted file mode 100644 index 9789cd2cf..000000000 --- a/apps/client/src/pages/onboarding/onboarding-page.css.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { style } from '@vanilla-extract/css'; - -export const startBottomContainer = style({ - position: 'fixed', - bottom: 0, - - maxWidth: '43rem', - width: '100vw', - display: 'flex', - - flexDirection: 'column', - alignItems: 'center', - padding: '0 1.6rem 2.4rem', - gap: '1.6rem', -}); - -export const defaultButtonContainer = style({ - position: 'fixed', - bottom: 0, - - maxWidth: '43rem', - width: '100vw', - - padding: '0 1.6rem 2.4rem', -}); diff --git a/apps/client/src/pages/onboarding/onboarding-page.tsx b/apps/client/src/pages/onboarding/onboarding-page.tsx index 99c1d14c9..98da3f3fa 100644 --- a/apps/client/src/pages/onboarding/onboarding-page.tsx +++ b/apps/client/src/pages/onboarding/onboarding-page.tsx @@ -1,49 +1,73 @@ -import { useState } from 'react'; +import { zodResolver } from '@hookform/resolvers/zod'; import { useSuspenseQuery } from '@tanstack/react-query'; +import { FormProvider, useForm } from 'react-hook-form'; import { useNavigate } from 'react-router-dom'; -import { Button, TextButton, toasts } from '@bds/ui'; -import { Navigation } from '@bds/ui'; +import { Navigation, toasts } from '@bds/ui'; import { useModal } from '@bds/ui'; import { Icon } from '@bds/ui/icons'; import InsuranceNoticeModal from '@widgets/onboarding/components/insurance-notice-modal/insurance-notice-modal'; import ProgressBar from '@widgets/onboarding/components/progress-bar/progress-bar'; import CoverageInfo from '@widgets/onboarding/components/step/coverage-info/coverage-info'; +import EtceteraInfo from '@widgets/onboarding/components/step/etcetera-info/etcetera-info'; import HealthInfo from '@widgets/onboarding/components/step/health-info/health-info'; import MatchingLoader from '@widgets/onboarding/components/step/matching-loader/matching-loader'; import PriceInfo from '@widgets/onboarding/components/step/price-info/price-info'; import StartContent from '@widgets/onboarding/components/step/start-content/start-content'; import UserInfo from '@widgets/onboarding/components/step/user-info/user-info'; -import { UserInfoStateProps } from '@widgets/onboarding/type/user-info.type'; +import { + onboardingDefaultValues, + onboardingFormSchema, + type onboardingFormType, +} from '@widgets/onboarding/schemas/onboarding-form-schema'; import { buildSubmitPayload } from '@widgets/onboarding/utils/build-submit-payload'; import { usePostUserInfo, USER_QUERY_OPTIONS, } from '@shared/api/domain/onboarding/queries'; +import { SwitchCase } from '@shared/components/switch-case'; import { useFunnel } from '@shared/hooks/use-funnel'; -import { useUserInfoValid } from '@shared/hooks/use-user-info-valid'; import { routePath } from '@shared/router/path'; -import * as styles from './onboarding-page.css'; - -const initialState: UserInfoStateProps = { - name: '', - birthYear: '', - birthMonth: '', - birthDay: '', - gender: '์—ฌ์„ฑ', - occupation: '', - isMarried: false, - hasChild: false, - isDriver: false, -}; - -const stepSlugs = ['start', 'user', 'health', 'coverage', 'price', 'matching']; +const stepSlugs = [ + 'start', + 'user', + 'health', + 'coverage', + 'price', + 'etc', + 'matching', +]; const completePath = routePath.REPORT; const OnboardingPage = () => { + const { Funnel, Step, go, currentStep, currentIndex } = useFunnel( + stepSlugs, + completePath, + ); + + const methods = useForm({ + resolver: zodResolver(onboardingFormSchema), + mode: 'onChange', + defaultValues: onboardingDefaultValues, + }); + const { + watch, + getValues, + handleSubmit, + formState: { errors }, + } = methods; + + const { openModal, closeModal } = useModal(); + const navigate = useNavigate(); + const handleGoHome = () => navigate(routePath.HOME); + + const progressIndex = Math.max(currentIndex - 1, 0); + const excluded = ['start', 'matching']; + const progressTotal = stepSlugs.filter((s) => !excluded.includes(s)).length; + const { data: userData } = useSuspenseQuery(USER_QUERY_OPTIONS.PROFILE()); const { data: userJobs } = useSuspenseQuery(USER_QUERY_OPTIONS.JOBS()); const { data: userDiseases } = useSuspenseQuery( @@ -52,26 +76,68 @@ const OnboardingPage = () => { const { data: userCoverages } = useSuspenseQuery( USER_QUERY_OPTIONS.COVERAGES(), ); - const navigate = useNavigate(); - - const { openModal, closeModal } = useModal(); - const isRecommended = userData?.data?.isRecommendInsurance; - - if (isRecommended) { - navigate(routePath.HOME); - } const { mutate } = usePostUserInfo(() => { navigate(routePath.REPORT); }); + const handleLimitExceed = () => { + toasts.show({ + message: '3์ˆœ์œ„๊นŒ์ง€๋งŒ ์„ ํƒํ•  ์ˆ˜ ์žˆ์–ด์š”', + duration: 3000, + icon: , + }); + }; + + const isNextEnabled = (() => { + switch (currentStep) { + case 'user': { + const { name, gender, job } = watch(); + return !!name && !!gender && !!job && !errors.name; + } + case 'health': { + const health = watch('health'); + return ( + (health.self?.length ?? 0) > 0 && (health.family?.length ?? 0) > 0 + ); + } + case 'coverage': { + const indices = watch('coverageIndices'); + return (indices?.length ?? 0) >= 1; + } + case 'price': { + const [min, max] = watch('priceRange') ?? [7, 15]; + return min < max; + } + default: + return true; + } + })(); + const handlePostUserInfo = () => { + const form = getValues(); const payload = buildSubmitPayload({ - basicInfoState, - healthFirstSelected, - healthSecondSelected, - coverageSelected, - priceRange, + basicInfoState: { + name: form.name, + birthYear: form.birthYear, + birthMonth: form.birthMonth, + birthDay: form.birthDay, + gender: form.gender, + occupation: form.job, + isMarried: form.isMarried, + hasChild: form.hasChild, + isDriver: form.isDriver, + }, + healthFirstSelected: form.health.self, + healthSecondSelected: form.health.family, + coverageSelected: form.coverageIndices, + priceRange: form.priceRange, + + renewableType: form.renewableType, + refundType: form.refundType, + paymentPeriod: form.paymentPeriod, + maturityAge: form.maturityAge, + userJobs: userJobs?.data?.jobs ?? [], diagnosedDiseases: userDiseases?.data?.diagnosedDiseases ?? [], coverageItems: userCoverages?.data?.coveragePreferenceResponses ?? [], @@ -80,47 +146,7 @@ const OnboardingPage = () => { mutate(payload); }; - const { Funnel, Step, go, currentStep, currentIndex } = useFunnel( - stepSlugs, - completePath, - ); - const progressIndex = Math.max(currentIndex - 1, 0); - const progressTotal = 4; - - const [basicInfoState, setBasicInfoState] = - useState(initialState); - const [healthFirstSelected, setHealthFirstSelected] = useState([]); - const [healthSecondSelected, setHealthSecondSelected] = useState( - [], - ); - const [coverageSelected, setCoverageSelected] = useState([]); - - const [priceRange, setPriceRange] = useState<[number, number]>([7, 15]); - - const isUserValid = useUserInfoValid(basicInfoState); - - const isHealthValid = - healthFirstSelected.length > 0 && healthSecondSelected.length > 0; - - const handleCoverageSelectionChange = (selectedIndices: number[]) => { - setCoverageSelected(selectedIndices); - }; - - const handleGo = (step: number) => { - go(step); - }; - - const isNeedTermsAgreement = () => currentStep === 'price'; - - const handleNext = () => { - if (isNeedTermsAgreement()) { - openTermsModal(); - } else { - go(1); - } - }; - - const openTermsModal = () => { + const handleFormSubmit = () => { openModal( { @@ -132,111 +158,80 @@ const OnboardingPage = () => { ); }; - const handleGoHome = () => navigate(routePath.HOME); - - const handleLimitExceed = () => { - toasts.show({ - message: '3์ˆœ์œ„๊นŒ์ง€๋งŒ ์„ ํƒํ•  ์ˆ˜ ์žˆ์–ด์š”', - duration: 3000, - icon: , - }); - }; - - const stepValidationMap: Record = { - start: true, - user: isUserValid, - health: isHealthValid, - coverage: coverageSelected.length > 0, - }; - - const isNextEnabled = stepValidationMap[currentStep] ?? true; - return (
- {currentStep !== 'matching' && ( - : undefined - } - onClickLeft={() => handleGo(-1)} - rightIcon={} - onClickRight={handleGoHome} - title="์ •๋ณด์ž…๋ ฅ" - /> - )} - - {currentStep !== 'start' && currentStep !== 'matching' && ( - - )} - - - - - - - - - - - - - - - - - - - - - - - {currentStep !== 'matching' && ( -
- {currentStep === 'start' ? ( - <> - - - ๋‚˜์ค‘์— ์ถ”์ฒœ๋ฐ›์„๋ž˜์š” - - - ) : ( - - )} -
- )} + ( + } + onClickRight={handleGoHome} + title="์ •๋ณด์ž…๋ ฅ" + /> + ), + matching: () => null, + }} + defaultComponent={() => ( + <> + } + onClickLeft={() => go(-1)} + rightIcon={} + onClickRight={handleGoHome} + title="์ •๋ณด์ž…๋ ฅ" + /> + + + )} + /> + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+
); }; diff --git a/apps/client/src/pages/splash/splash-page.css.ts b/apps/client/src/pages/splash/splash-page.css.ts index 7ce393ecc..9fbaf16b8 100644 --- a/apps/client/src/pages/splash/splash-page.css.ts +++ b/apps/client/src/pages/splash/splash-page.css.ts @@ -9,3 +9,13 @@ export const container = style({ justifyContent: 'center', gap: '1.5rem', }); + +export const logo = style({ + width: '14rem', + height: '10.7rem', +}); + +export const logotype = style({ + width: '15.9rem', + height: '5.4rem', +}); diff --git a/apps/client/src/pages/splash/splash-page.tsx b/apps/client/src/pages/splash/splash-page.tsx index 07786d6a8..463820710 100644 --- a/apps/client/src/pages/splash/splash-page.tsx +++ b/apps/client/src/pages/splash/splash-page.tsx @@ -1,8 +1,6 @@ import { useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; -import { Icon } from '@bds/ui/icons'; - import { routePath } from '@shared/router/path'; import * as styles from './splash-page.css'; @@ -21,8 +19,12 @@ const SplashPage = () => { return (
- - + ๋ณดํ• ๋กœ๊ณ  + ๋ณดํ• ๋กœ๊ณ ํƒ€์ž…
); }; diff --git a/apps/client/src/shared/api/config/end-point.ts b/apps/client/src/shared/api/config/end-point.ts index aee91a441..7a5b3ebbc 100644 --- a/apps/client/src/shared/api/config/end-point.ts +++ b/apps/client/src/shared/api/config/end-point.ts @@ -7,7 +7,27 @@ export const END_POINT = { DELETE_FEED: 'posts', GET_COMMENTS: (postId?: string) => `posts/${postId}/comments`, POST_COMMENTS: (postId?: string) => `posts/${postId}/comments`, + PATCH_COMMENTS: (postId: string, commentId: number) => + `posts/${postId}/comments/${commentId}`, DELETE_COMMENTS: 'posts', + GET_COMMENT_REPLY: (postId: string, commentId: number) => + `posts/${postId}/comments/${commentId}/reply`, + POST_COMMENT_REPLY: (postId: string, commentId: number) => + `posts/${postId}/comments/${commentId}/reply`, + PATCH_COMMENT_REPLY: ( + postId: string, + commentId: number, + commentReplyId: number, + ) => `posts/${postId}/comments/${commentId}/reply/${commentReplyId}`, + DELETE_COMMENT_REPLY: ( + postId: string, + commentId: number, + commentReplyId: number, + ) => `posts/${postId}/comments/${commentId}/reply/${commentReplyId}`, + GET_SEARCH: `posts/search`, + POST_LIKE: (postId: string) => `posts/${postId}/likes`, + DELETE_LIKE: (postId: string) => `posts/${postId}/likes`, + GET_POPULAR: 'posts/trend', }, USER: { GET_USER_INFO: 'users/info', @@ -16,8 +36,10 @@ export const END_POINT = { GET_ME_COMMENTS: 'users/me/comments', GET_USER_INFO_DISEASES: 'user-infos/diagnosed-disease', GET_USER_INFO_COVERAGES: 'user-infos/coverage-select', + GET_USER_INFO_OPTIONS: 'user-infos/insurances/options', GET_REPORT_SUMMARY: 'users/me/report-summary', POST_USER_INFO_SUBMIT: 'insurances/reports', + PATCH_USER_INFO: 'users', }, INSURANCE: { GET_REPORT: (id: string) => `insurances/reports/${id}`, @@ -30,4 +52,11 @@ export const END_POINT = { GET_JANGHAE_REPORT: (id: string) => `insurances/reports/${id}/disability`, GET_SAMANG_REPORT: (id: string) => `insurances/reports/${id}/death`, }, + AUTH: { + KAKAO_LOGOUT: 'oauth/kakao/logout', + KAKAO_WITHDRAW: 'oauth/kakao/unlink', + }, + SHARED: { + IMAGE_UPLOAD: 'files/upload', + }, }; diff --git a/apps/client/src/shared/api/domain/community/queries.ts b/apps/client/src/shared/api/domain/community/queries.ts index 2eed18819..66ce29d94 100644 --- a/apps/client/src/shared/api/domain/community/queries.ts +++ b/apps/client/src/shared/api/domain/community/queries.ts @@ -12,7 +12,15 @@ import { } from '@shared/api/keys/query-key'; import { CommentDeleteResponse, + CommentPatchRequest, + CommentPatchResponse, CommentPostResponse, + CommentReplyDeleteRequest, + CommentReplyDeleteResponse, + CommentReplyPatchRequest, + CommentReplyPatchResponse, + CommentReplyPostResponse, + CommentReplyResponse, CommentResponse, FeedDeleteResponse, FeedDetailResponse, @@ -21,6 +29,10 @@ import { FeedResponse, FeedUpdateRequestBody, FeedUpdateResponse, + LikeAddResponse, + LikeDeleteResponse, + PopularFeedResponse, + SearchGetResponse, } from '@shared/api/types/types'; // ============================================================================= @@ -28,11 +40,11 @@ import { // ============================================================================= export const COMMUNITY_QUERY_OPTIONS = { - POSTS: () => + POSTS: (sort: string, category: string) => infiniteQueryOptions({ - queryKey: COMMUNITY_QUERY_KEY.FEED_PREVIEW(), + queryKey: COMMUNITY_QUERY_KEY.FEED_PREVIEW(sort, category), queryFn: ({ pageParam = 0 }) => - getAllFeed({ pageParam: pageParam as number }), + getAllFeed({ pageParam: pageParam as number }, sort, category), getNextPageParam: (lastPage) => lastPage?.isLast ? undefined : lastPage?.nextCursor, initialPageParam: 0, @@ -53,6 +65,32 @@ export const COMMUNITY_QUERY_OPTIONS = { queryFn: () => getFeedDetail(postId), }); }, + + COMMENT_REPLY: (postId: string, commentId: number) => + infiniteQueryOptions({ + queryKey: COMMUNITY_QUERY_KEY.COMMENTS_REPLY(postId, commentId), + queryFn: ({ pageParam = 0 }) => + getCommentReply(postId, commentId, { pageParam }), + getNextPageParam: (lastPage) => + lastPage?.data?.nextCursor ? lastPage.data.nextCursor : undefined, + initialPageParam: 0, + }), + + POPULAR_FEED: (size?: number) => { + return queryOptions({ + queryKey: COMMUNITY_QUERY_KEY.POPULAR_FEED(), + queryFn: () => getPopularFeed(size), + }); + }, + SEARCH: (keyword: string) => + infiniteQueryOptions({ + queryKey: COMMUNITY_QUERY_KEY.SEARCH(keyword), + queryFn: ({ pageParam = '' }) => getSearch(keyword, { pageParam }), + getNextPageParam: (lastPage) => + lastPage?.data?.isLast ? undefined : lastPage?.data?.nextCursor, + initialPageParam: '', + enabled: keyword.trim().length > 0, + }), }; // ============================================================================= @@ -65,13 +103,15 @@ export const COMMUNITY_QUERY_OPTIONS = { * @param options.pageParam - ํŽ˜์ด์ง€ ํŒŒ๋ผ๋ฏธํ„ฐ (๊ธฐ๋ณธ๊ฐ’: 0) * @returns ๊ฒŒ์‹œ๊ธ€ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์‘๋‹ต ๋ฐ์ดํ„ฐ */ -export const getAllFeed = async ({ - pageParam, -}: { pageParam?: number } = {}): Promise => { +export const getAllFeed = async ( + { pageParam }: { pageParam?: number } = {}, + sort: string, + category: string, +): Promise => { const url = pageParam === 0 - ? `${END_POINT.COMMUNITY.GET_FEED}?size=10` - : `${END_POINT.COMMUNITY.GET_FEED}?cursor=${pageParam}&size=10`; + ? `${END_POINT.COMMUNITY.GET_FEED}?sort=${sort}&category=${category}&size=10` + : `${END_POINT.COMMUNITY.GET_FEED}?sort=${sort}&category=${category}&cursor=${pageParam}&size=10`; const response = await api.get(url).json(); @@ -114,6 +154,61 @@ export const getFeedDetail = async ( return response.data; }; +/** + * + * @param postId - ๋Œ“๊ธ€์ด ์†ํ•œ ๊ฒŒ์‹œ๊ธ€ ID + * @param commentId - ๋Œ“๊ธ€ ID + * @param options - ํŽ˜์ด์ง€๋„ค์ด์…˜ ์˜ต์…˜ + * @param options.pageParam - ํŽ˜์ด์ง€ ํŒŒ๋ผ๋ฏธํ„ฐ (๊ธฐ๋ณธ๊ฐ’: 0) + * @returns ๋Œ€๋Œ“๊ธ€ ์‘๋‹ต ๋ฐ์ดํ„ฐ ๋˜๋Š” null + */ + +export const getCommentReply = async ( + postId: string, + commentId: number, + { pageParam }: { pageParam?: number } = {}, +): Promise => { + const url = + pageParam === 0 + ? `${END_POINT.COMMUNITY.GET_COMMENT_REPLY(postId, commentId)}?size=10` + : `${END_POINT.COMMUNITY.GET_COMMENT_REPLY(postId, commentId)}?cursor=${pageParam}&size=10`; + const response = await api.get(url).json(); + return response; +}; + +/** + * ์ธ๊ธฐ ๊ฒŒ์‹œ๊ธ€์˜ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. + * @param size - ๊ฒŒ์‹œ๊ธ€ ๊ฒ€์ƒ‰ ํŒŒ๋ผ๋ฏธํ„ฐ + * @returns ๊ฒŒ์‹œ๊ธ€ ์ƒ์„ธ ์‘๋‹ต ๋ฐ์ดํ„ฐ ๋˜๋Š” null + */ +export const getPopularFeed = async ( + size?: number, + sort?: string, +): Promise => { + const response = await api + .get(`${END_POINT.COMMUNITY.GET_POPULAR}?size=${size}&sort=${sort}`) + .json(); + return response; +}; + +/** + * ๊ฒ€์ƒ‰ ํ‚ค์›Œ๋“œ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ฒŒ์‹œ๋ฌผ์„ ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค. + * @param keyword - ๊ฒŒ์‹œ๊ธ€ ๊ฒ€์ƒ‰ ํŒŒ๋ผ๋ฏธํ„ฐ + * @returns ๊ฒŒ์‹œ๊ธ€ ์ƒ์„ธ ์‘๋‹ต ๋ฐ์ดํ„ฐ ๋˜๋Š” null + */ +export const getSearch = async ( + keyword: string, + { pageParam = '' }: { pageParam?: string } = {}, +): Promise => { + const url = + pageParam === '' + ? `${END_POINT.COMMUNITY.GET_SEARCH}?keyword=${keyword}&size=10` + : `${END_POINT.COMMUNITY.GET_SEARCH}?keyword=${keyword}&cursor=${pageParam}&size=10`; + const response = await api.get(url).json(); + + return response; +}; + // ============================================================================= // MUTATION OPTIONS // ============================================================================= @@ -126,9 +221,9 @@ export const COMMUNITY_MUTATION_OPTIONS = { }); }, - POST_FEED: () => { + POST_FEED: (sort?: string, category?: string) => { return mutationOptions({ - mutationKey: COMMUNITY_MUTATION_KEY.POST_FEED(), + mutationKey: COMMUNITY_MUTATION_KEY.POST_FEED(sort, category), mutationFn: postFeed, }); }, @@ -151,7 +246,53 @@ export const COMMUNITY_MUTATION_OPTIONS = { DELETE_COMMENT: (postId: string) => { return mutationOptions({ mutationKey: COMMUNITY_MUTATION_KEY.DELETE_COMMENT(postId), - mutationFn: (commentId?: string) => deleteComment(postId, commentId), + mutationFn: (commentId?: number) => deleteComment(postId, commentId), + }); + }, + + DELETE_COMMENT_REPLY: (postId: string) => { + return mutationOptions({ + mutationKey: COMMUNITY_MUTATION_KEY.DELETE_COMMENT_REPLY(postId), + mutationFn: ({ + ['comment-id']: commentId, + ['comment-reply-id']: commentReplyId, + }: CommentReplyDeleteRequest) => + deleteCommentReply(postId, commentId, commentReplyId), + }); + }, + + ADD_LIKE: (postId: string) => { + return mutationOptions({ + mutationKey: COMMUNITY_MUTATION_KEY.ADD_LIKE(postId), + mutationFn: () => postLike(postId), + }); + }, + + DELETE_LIKE: (postId: string) => { + return mutationOptions({ + mutationKey: COMMUNITY_MUTATION_KEY.DELETE_LIKE(postId), + mutationFn: () => deleteLike(postId), + }); + }, + + POST_COMMENT_REPLY: () => { + return mutationOptions({ + mutationKey: COMMUNITY_MUTATION_KEY.POST_COMMENT_REPLY(), + mutationFn: postCommentReply, + }); + }, + + PATCH_COMMENT: () => { + return mutationOptions({ + mutationKey: COMMUNITY_MUTATION_KEY.PATCH_COMMENT(), + mutationFn: patchComment, + }); + }, + + PATCH_COMMENT_REPLY: () => { + return mutationOptions({ + mutationKey: COMMUNITY_MUTATION_KEY.PATCH_COMMENT_REPLY(), + mutationFn: patchCommentReply, }); }, }; @@ -165,21 +306,45 @@ export const COMMUNITY_MUTATION_OPTIONS = { * @param params - ๋Œ“๊ธ€ ์ž‘์„ฑ ํŒŒ๋ผ๋ฏธํ„ฐ * @param params.postId - ๋Œ“๊ธ€์„ ์ž‘์„ฑํ•  ๊ฒŒ์‹œ๊ธ€ ID * @param params.content - ๋Œ“๊ธ€ ๋‚ด์šฉ + * @param params.imageUrls - ๋Œ“๊ธ€ ์ด๋ฏธ์ง€ ๋ฐฐ์—ด (์„ ํƒ) * @returns ๋Œ“๊ธ€ ์ž‘์„ฑ ์‘๋‹ต ๋ฐ์ดํ„ฐ */ export const postComment = async (params: { postId: string; content: string; + imageUrls?: string[]; }): Promise => { - const { postId, content } = params; + const { postId, content, imageUrls } = params; return api .post(END_POINT.COMMUNITY.POST_COMMENTS(postId), { - json: { content }, + json: { content, imageUrls: imageUrls ?? [] }, }) .json(); }; +/** + * ๊ฒŒ์‹œ๊ธ€ ๋Œ“๊ธ€์„ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค. + * @param params - ๋Œ“๊ธ€ ์ˆ˜์ • ํŒŒ๋ผ๋ฏธํ„ฐ + * @param params.postId - ๋Œ“๊ธ€์„ ์ˆ˜์ •ํ•  ๊ฒŒ์‹œ๊ธ€ ID + * @param params.commentId - ์ˆ˜์ •ํ•  ๋Œ“๊ธ€ ID + * @param params.body - ๋Œ“๊ธ€ ์ˆ˜์ • ๋‚ด์šฉ + * @returns ๋Œ“๊ธ€ ์ˆ˜์ • ์‘๋‹ต ๋ฐ์ดํ„ฐ + */ +export const patchComment = async (params: { + postId: string; + commentId: number; + body: CommentPatchRequest; +}): Promise => { + const { postId, commentId, body } = params; + + return api + .patch(END_POINT.COMMUNITY.PATCH_COMMENTS(postId, commentId), { + json: body, + }) + .json(); +}; + /** * ์ƒˆ ๊ฒŒ์‹œ๊ธ€์„ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. * @param body - ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ ์š”์ฒญ ๋ฐ์ดํ„ฐ @@ -230,7 +395,7 @@ export const deleteFeed = async ( */ export const deleteComment = async ( postId?: string, - commentId?: string, + commentId?: number, ): Promise => { const response = await api .delete( @@ -239,3 +404,102 @@ export const deleteComment = async ( .json(); return response; }; + +/** + * ๊ฒŒ์‹œ๋ฌผ ๋Œ“๊ธ€์— ๋Œ€๋Œ“๊ธ€์„ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. + * @param postId - ๋Œ“๊ธ€์ด ์†ํ•œ ๊ฒŒ์‹œ๊ธ€ ID + * @param commentId ๋Œ€๋Œ“๊ธ€์„ ์ถ”๊ฐ€ํ•  ๋Œ“๊ธ€ ID + * @returns ๋Œ€๋Œ“๊ธ€ ์ถ”๊ฐ€ ์‘๋‹ต ๋ฐ์ดํ„ฐ + */ +export const postCommentReply = async (params: { + postId: string; + commentId: number; + content: string; + imageUrls: string[]; +}): Promise => { + const { postId, commentId, content, imageUrls } = params; + return api + .post(END_POINT.COMMUNITY.POST_COMMENT_REPLY(postId, commentId), { + json: { content, imageUrls }, + }) + .json(); +}; + +/** + * ๊ฒŒ์‹œ๋ฌผ์˜ ๋Œ€๋Œ“๊ธ€์„ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค. + * @param postId - ๋Œ“๊ธ€์ด ์†ํ•œ ๊ฒŒ์‹œ๊ธ€ ID + * @param commentId - ๋Œ“๊ธ€์˜ ID + * @param commentReplyId - ๋Œ€๋Œ“๊ธ€ ID + * @body - ๋Œ“๋Œ“๊ธ€ ์ˆ˜์ • ๋‚ด์šฉ + * @retruns - ๋Œ€๋Œ“๊ธ€ ์ถ”๊ฐ€ ์‘๋‹ต ๋ฐ์ดํ„ฐ + */ +export const patchCommentReply = async (params: { + postId: string; + commentId: number; + commentReplyId: number; + body: CommentReplyPatchRequest; +}): Promise => { + const { postId, commentId, commentReplyId, body } = params; + return api + .patch( + END_POINT.COMMUNITY.PATCH_COMMENT_REPLY( + postId, + commentId, + commentReplyId, + ), + { + json: body, + }, + ) + .json(); +}; + +/** + * ๋Œ€๋Œ“๊ธ€์„ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค. + * @param postId - ๋Œ€๋Œ“๊ธ€์ด ์†ํ•œ ๊ฒŒ์‹œ๊ธ€ ID + * @param commentId - ๋Œ€๋Œ“๊ธ€์ด ์†ํ•œ ๋Œ“๊ธ€ ID + * @param commentReplyId - ์‚ญ์ œํ•  ๋Œ€๋Œ“๊ธ€ ID + * @returns ๋Œ€๋Œ“๊ธ€ ์‚ญ์ œ ์‘๋‹ต ๋ฐ์ดํ„ฐ + */ +export const deleteCommentReply = async ( + postId: string, + commentId: number, + commentReplyId: number, +): Promise => { + const response = await api + .delete( + END_POINT.COMMUNITY.DELETE_COMMENT_REPLY( + postId, + commentId, + commentReplyId, + ), + ) + .json(); + return response; +}; + +/** + * ๊ฒŒ์‹œ๊ธ€์— ์ข‹์•„์š”๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. + * @param postId - ์ข‹์•„์š”๋ฅผ ๋ˆ„๋ฅผ ๊ฒŒ์‹œ๊ธ€ ID + * @returns ์ข‹์•„์š” ์ƒ์„ฑ ์‘๋‹ต ๋ฐ์ดํ„ฐ + */ +export const postLike = async (postId: string): Promise => { + const response = await api + .post(END_POINT.COMMUNITY.POST_LIKE(postId)) + .json(); + return response; +}; + +/** + * ๊ฒŒ์‹œ๊ธ€์˜ ์ข‹์•„์š”๋ฅผ ์ทจ์†Œํ•ฉ๋‹ˆ๋‹ค. + * @param postId - ์ข‹์•„์š”๋ฅผ ์ทจ์†Œํ•  ๊ฒŒ์‹œ๊ธ€ ID + * @returns ์ข‹์•„์š” ์‚ญ์ œ ์‘๋‹ต ๋ฐ์ดํ„ฐ + */ +export const deleteLike = async ( + postId: string, +): Promise => { + const response = await api + .delete(END_POINT.COMMUNITY.DELETE_LIKE(postId)) + .json(); + return response; +}; diff --git a/apps/client/src/shared/api/domain/mypage/queries.ts b/apps/client/src/shared/api/domain/mypage/queries.ts index 90f3ce0c2..75b4028e6 100644 --- a/apps/client/src/shared/api/domain/mypage/queries.ts +++ b/apps/client/src/shared/api/domain/mypage/queries.ts @@ -1,9 +1,27 @@ -import { infiniteQueryOptions, queryOptions } from '@tanstack/react-query'; +import { + infiniteQueryOptions, + mutationOptions, + queryOptions, +} from '@tanstack/react-query'; import { END_POINT } from '@shared/api/config/end-point.ts'; import { api } from '@shared/api/config/instance'; -import { USER_QUERY_KEY } from '@shared/api/keys/query-key.ts'; -import { MePostResponse, UserProfile } from '@shared/api/types/types'; +import { + USER_MUTATION_KEY, + USER_QUERY_KEY, +} from '@shared/api/keys/query-key.ts'; +import { + KakaoLogoutResponse, + KakaoWithdrawResponse, + MePostResponse, + UserProfile, + UserProfileEditRequestBody, + UserProfileEditResponse, +} from '@shared/api/types/types'; + +// ============================================================================= +// QUERY OPTIONS +// ============================================================================= export const USER_QUERY_OPTIONS = { PROFILE: () => @@ -31,6 +49,10 @@ export const USER_QUERY_OPTIONS = { }), }; +// ============================================================================= +// QUERY FUNCTIONS +// ============================================================================= + export const getUserProfile = async (): Promise => { const response = await api .get(END_POINT.USER.GET_USER_INFO) @@ -57,3 +79,55 @@ export const getMeComments = async ({ pageParam }: { pageParam: number }) => { const response = await api.get(url).json(); return response.data; }; + +// ============================================================================= +// MUTATION OPTIONS +// ============================================================================= + +export const USER_MUTATION_OPTIONS = { + KAKAO_LOGOUT: () => { + return mutationOptions({ + mutationKey: USER_QUERY_KEY.KAKAO_LOGOUT(), + mutationFn: kakaoLogout, + }); + }, + + KAKAO_WITHDRAW: () => { + return mutationOptions({ + mutationKey: USER_QUERY_KEY.KAKAO_WITHDRAW(), + mutationFn: kakaoWithdraw, + }); + }, + PATCH_USER_PROFILE: () => { + return mutationOptions({ + mutationKey: USER_MUTATION_KEY.USER_PROFILE(), + mutationFn: ({ body }: { body: UserProfileEditRequestBody }) => + patchUserProfile(body), + }); + }, +}; + +// ============================================================================= +// MUTATION FUNCTIONS +// ============================================================================= + +export const kakaoLogout = async (redirectUrl: string) => { + const response = await api + .post(`${END_POINT.AUTH.KAKAO_LOGOUT}?redirect-url=${redirectUrl}`) + .json(); + return response; +}; + +export const kakaoWithdraw = async () => { + const response = await api + .delete(END_POINT.AUTH.KAKAO_WITHDRAW) + .json(); + return response; +}; + +export const patchUserProfile = async (data: UserProfileEditRequestBody) => { + const response = await api + .patch(END_POINT.USER.PATCH_USER_INFO, { json: data }) + .json(); + return response; +}; diff --git a/apps/client/src/shared/api/domain/onboarding/queries.ts b/apps/client/src/shared/api/domain/onboarding/queries.ts index de9172909..5a5309b2d 100644 --- a/apps/client/src/shared/api/domain/onboarding/queries.ts +++ b/apps/client/src/shared/api/domain/onboarding/queries.ts @@ -7,6 +7,7 @@ import { UserInfoCoverages, UserInfoDiseases, UserInfoJobs, + UserInfoOptions, UserInfoSubmitRequest, UserInfoSubmitResponse, UserProfile, @@ -37,6 +38,12 @@ export const USER_QUERY_OPTIONS = { queryFn: getUserInfoCoverages, }); }, + OPTIONS: () => { + return queryOptions({ + queryKey: USER_QUERY_KEY.OPTIONS(), + queryFn: getUserInfoOptions, + }); + }, }; export const usePostUserInfo = (onSuccessCallback?: () => void) => { @@ -80,6 +87,13 @@ export const getUserInfoCoverages = return response; }; +export const getUserInfoOptions = async (): Promise => { + const response = await api + .get(END_POINT.USER.GET_USER_INFO_OPTIONS) + .json(); + return response; +}; + export const postUserInfo = async ( body: UserInfoSubmitRequest, ): Promise => { diff --git a/apps/client/src/shared/api/domain/queries.ts b/apps/client/src/shared/api/domain/queries.ts new file mode 100644 index 000000000..ce1956dce --- /dev/null +++ b/apps/client/src/shared/api/domain/queries.ts @@ -0,0 +1,34 @@ +import { mutationOptions } from '@tanstack/react-query'; +import ky from '@toss/ky'; + +import { END_POINT } from '../config/end-point'; +import { api } from '../config/instance'; +import { SHARED_MUTATION_KEY } from '../keys/query-key'; +import { ImageUploadResponse } from '../types/types'; + +export const MUTATION_QUERY_OPTIONS = { + POST_IMAGE: () => { + return mutationOptions({ + mutationKey: SHARED_MUTATION_KEY.IMAGE_UPLOAD(), + mutationFn: (mediaType: string[]) => postImage(mediaType), + }); + }, +}; + +// ============================================================================= +// MUTATION FUNCTIONS +// ============================================================================= + +export const postImage = async (mediaType: string[]) => { + const response = await api + .post(END_POINT.SHARED.IMAGE_UPLOAD, { json: { mediaType } }) + .json(); + + return response.data; +}; + +export const uploadImageToS3 = async (url: string, file: File) => { + await ky.put(url, { + body: file, + }); +}; diff --git a/apps/client/src/shared/api/keys/query-key.ts b/apps/client/src/shared/api/keys/query-key.ts index 8fdf30c99..4ba87c943 100644 --- a/apps/client/src/shared/api/keys/query-key.ts +++ b/apps/client/src/shared/api/keys/query-key.ts @@ -18,11 +18,24 @@ export const USER_QUERY_KEY = { ME_COMMENTS: () => [...USER_QUERY_KEY.ALL, 'me-comments'], DISEASES: () => [...USER_QUERY_KEY.ALL, 'diseases'], COVERAGES: () => [...USER_QUERY_KEY.ALL, 'coverages'], + OPTIONS: () => [...USER_QUERY_KEY.ALL, 'options'], + KAKAO_LOGOUT: () => [...USER_QUERY_KEY.ALL, 'kakao-logout'], + KAKAO_WITHDRAW: () => [...USER_QUERY_KEY.ALL, 'kakao-withdraw'], } as const; +export const USER_MUTATION_KEY = { + ALL: ['users'], + USER_PROFILE: () => [...USER_MUTATION_KEY.ALL, 'user-profile'], +}; + export const COMMUNITY_QUERY_KEY = { ALL: ['community'], - FEED_PREVIEW: () => [...COMMUNITY_QUERY_KEY.ALL, 'feed'], + FEED_PREVIEW: (sort?: string, category?: string) => [ + ...COMMUNITY_QUERY_KEY.ALL, + 'feed', + sort, + category, + ], FEED_DETAIL: (postId: string) => [ ...COMMUNITY_QUERY_KEY.ALL, 'detail', @@ -33,11 +46,23 @@ export const COMMUNITY_QUERY_KEY = { 'comment', postId, ], + COMMENTS_REPLY: (postId: string, commentId: number) => [ + ...COMMUNITY_QUERY_KEY.ALL, + 'comment', + postId, + 'reply', + commentId, + ], + POPULAR_FEED: () => [...COMMUNITY_QUERY_KEY.ALL, 'popular'], + SEARCH: (keyword: string) => [...COMMUNITY_QUERY_KEY.ALL, 'search', keyword], } as const; export const COMMUNITY_MUTATION_KEY = { POST_COMMENT: () => [...COMMUNITY_QUERY_KEY.COMMENTS(), 'create'], - POST_FEED: () => [...COMMUNITY_QUERY_KEY.FEED_PREVIEW(), 'create'], + POST_FEED: (sort?: string, category?: string) => [ + ...COMMUNITY_QUERY_KEY.FEED_PREVIEW(sort, category), + 'create', + ], PUT_FEED: (postId: string) => [ ...COMMUNITY_QUERY_KEY.FEED_DETAIL(postId), 'update', @@ -50,9 +75,48 @@ export const COMMUNITY_MUTATION_KEY = { ...COMMUNITY_QUERY_KEY.COMMENTS(postId), 'delete', ], + PATCH_COMMENT: () => [...COMMUNITY_QUERY_KEY.ALL, 'comment', 'update'], + POST_COMMENT_REPLY: () => [ + ...COMMUNITY_QUERY_KEY.ALL, + 'comment', + 'reply', + 'create', + ], + PATCH_COMMENT_REPLY: () => [ + ...COMMUNITY_QUERY_KEY.ALL, + 'comment', + 'reply', + 'update', + ], + DELETE_COMMENT_REPLY: (postId: string) => [ + ...COMMUNITY_QUERY_KEY.ALL, + 'comment', + postId, + 'reply', + 'delete', + ], + ADD_LIKE: (postId: string) => [ + ...COMMUNITY_QUERY_KEY.FEED_DETAIL(postId), + 'add', + ], + DELETE_LIKE: (postId: string) => [ + ...COMMUNITY_QUERY_KEY.FEED_DETAIL(postId), + 'delete', + ], } as const; export const HOME_QUERY_KEY = { ALL: ['home'], REPORT_SUMMARY: () => [...HOME_QUERY_KEY.ALL, 'report_summary'], } as const; + +export const AUTH_MUTATION_KEY = { + ALL: ['auth'], + KAKAO_LOGOUT: () => [...AUTH_MUTATION_KEY.ALL, 'kakao-logout'], + KAKAO_WITHDRAW: () => [...AUTH_MUTATION_KEY.ALL, 'kakao-withdraw'], +} as const; + +export const SHARED_MUTATION_KEY = { + ALL: ['shared'], + IMAGE_UPLOAD: () => [...SHARED_MUTATION_KEY.ALL, 'image-upload'], +}; diff --git a/apps/client/src/shared/api/types/types.ts b/apps/client/src/shared/api/types/types.ts index 24215accc..11999a546 100644 --- a/apps/client/src/shared/api/types/types.ts +++ b/apps/client/src/shared/api/types/types.ts @@ -34,6 +34,8 @@ export type UserInfoDiseases = export type UserInfoCoverages = paths['/user-infos/coverage-select']['get']['responses']['200']['content']['*/*']; +export type UserInfoOptions = + paths['/user-infos/insurances/options']['get']['responses']['200']['content']['*/*']; /** * @description ๋ณดํ—˜ ๋ฆฌํฌํŠธ ์ œ์ถœ ์š”์ฒญ ๋ฐ”๋”” */ @@ -46,6 +48,27 @@ export type UserInfoSubmitRequest = export type UserInfoSubmitResponse = paths['/insurances/reports']['post']['responses']['200']['content']['*/*']; +/** + * @description ์นด์นด์˜ค ๋กœ๊ทธ์•„์›ƒ ์‘๋‹ต + */ +export type KakaoLogoutResponse = + paths['/oauth/kakao/logout']['post']['responses']['200']['content']['*/*']; + +/** + * @description ์นด์นด์˜ค ํšŒ์›ํƒˆํ‡ด ์‘๋‹ต + */ +export type KakaoWithdrawResponse = + paths['/oauth/kakao/unlink']['delete']['responses']['200']['content']['*/*']; +/** + * @description ์œ ์ €์ •๋ณด ์ˆ˜์ • ์š”์ฒญ ๋ฐ”๋”” + */ +export type UserProfileEditRequestBody = + paths['/users']['patch']['requestBody']['content']['application/json']; +/** + * @description ์œ ์ €์ •๋ณด ์ˆ˜์ • ์‘๋‹ต + */ +export type UserProfileEditResponse = + paths['/users']['patch']['responses']['200']['content']['*/*']; /* ======================================================= * ๐Ÿ“Œ INSURANCE ๊ด€๋ จ ํƒ€์ž… * ======================================================= */ @@ -126,6 +149,18 @@ export type MePostRequest = export type CommentPostResponse = paths['/posts/{post-id}/comments']['post']['responses']['200']['content']; +/** + * @description ๋Œ“๊ธ€ ์ˆ˜์ • ์„ฑ๊ณต ์‘๋‹ต + */ +export type CommentPatchResponse = + paths['/posts/{post-id}/comments/{comment-id}']['patch']['responses']['200']['content']['*/*']; + +/** + * @description ๋Œ“๊ธ€ ์ˆ˜์ • ์š”์ฒญ ๋ฐ”๋”” + */ +export type CommentPatchRequest = + paths['/posts/{post-id}/comments/{comment-id}']['patch']['requestBody']['content']['application/json']; + /** * @description ๋Œ“๊ธ€ ์ž‘์„ฑ ์š”์ฒญ ๋ฐ”๋”” */ @@ -144,6 +179,18 @@ export type FeedDetailResponse = export type FeedPreviewResponse = paths['/posts']['get']['responses']['200']['content']['*/*']['data']; +/** + * @description ์ข‹์•„์š” ์ƒ์„ฑ ์„ฑ๊ณต ์‘๋‹ต + */ +export type LikeAddResponse = + paths['/posts/{post-id}/likes']['post']['responses']['200']['content']['*/*']; + +/** + * @description ์ข‹์•„์š” ์‚ญ์ œ ์„ฑ๊ณต ์‘๋‹ต + */ +export type LikeDeleteResponse = + paths['/posts/{post-id}/likes']['delete']['responses']['200']['content']['*/*']; + /** * @description ๋Œ“๊ธ€ ๋ชฉ๋ก ์กฐํšŒ ์‘๋‹ต */ @@ -162,6 +209,42 @@ export type CommentRequest = export type CommentDeleteResponse = paths['/posts/{post-id}/comments/{comment-id}']['delete']['responses']['200']['content']['*/*']; +/** + * @description ๋Œ€๋Œ“๊ธ€ ์กฐํšŒ ์‘๋‹ต + */ +export type CommentReplyResponse = + paths['/posts/{post-id}/comments/{comment-id}/reply']['get']['responses']['200']['content']['*/*']; + +/** + * @description ๋Œ€๋Œ“๊ธ€ ์ž‘์„ฑ ์„ฑ๊ณต ์‘๋‹ต + */ +export type CommentReplyPostResponse = + paths['/posts/{post-id}/comments/{comment-id}/reply']['post']['responses']['200']['content']['*/*']; + +/** + * @description ๋Œ€๋Œ“๊ธ€ ์ˆ˜์ • ์„ฑ๊ณต ์‘๋‹ต + */ +export type CommentReplyPatchResponse = + paths['/posts/{post-id}/comments/{comment-id}/reply/{comment-reply-id}']['patch']['responses']['200']['content']['*/*']; + +/** + * @description ๋Œ€๋Œ“๊ธ€ ์ˆ˜์ • ์š”์ฒญ + */ +export type CommentReplyPatchRequest = + paths['/posts/{post-id}/comments/{comment-id}/reply/{comment-reply-id}']['patch']['requestBody']['content']['application/json']; + +/** + * @description ๋Œ€๋Œ“๊ธ€ ์‚ญ์ œ ์„ฑ๊ณต ์‘๋‹ต + */ +export type CommentReplyDeleteResponse = + paths['/posts/{post-id}/comments/{comment-id}/reply/{comment-reply-id}']['delete']['responses']['200']['content']['*/*']; + +/** + * @description ๋Œ€๋Œ“๊ธ€ ์‚ญ์ œ ์š”์ฒญ ํŒŒ๋ผ๋ฏธํ„ฐ + */ +export type CommentReplyDeleteRequest = + paths['/posts/{post-id}/comments/{comment-id}/reply/{comment-reply-id}']['delete']['parameters']['path']; + /** * @description ํ”ผ๋“œ ์ˆ˜์ • ์š”์ฒญ ๊ฒฝ๋กœ ํŒŒ๋ผ๋ฏธํ„ฐ */ @@ -179,3 +262,21 @@ export type FeedUpdateRequestBody = */ export type FeedDeleteResponse = paths['/posts/{post-id}']['delete']['responses']['200']['content']['*/*']; + +/** + * @description ํ”ผ๋“œ ๊ฒ€์ƒ‰ ์„ฑ๊ณต ์‘๋‹ต + */ +export type SearchGetResponse = + paths['/posts/search']['get']['responses']['200']['content']['*/*']; + +/** + * @description ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ์‘๋‹ต + * */ +export type ImageUploadResponse = + paths['/files/upload']['post']['responses']['200']['content']['*/*']; + +/** + * @description ์‹ค์‹œ๊ฐ„ ์ธ๊ธฐ ๊ฒŒ์‹œ๊ธ€ ์กฐํšŒ ์‘๋‹ต + */ +export type PopularFeedResponse = + paths['/posts/trend']['get']['responses']['200']['content']['*/*']; diff --git a/apps/client/src/shared/components/insurance-subtitle/insurance-subtitle.tsx b/apps/client/src/shared/components/insurance-subtitle/insurance-subtitle.tsx index 2e5d886f5..71a6985e1 100644 --- a/apps/client/src/shared/components/insurance-subtitle/insurance-subtitle.tsx +++ b/apps/client/src/shared/components/insurance-subtitle/insurance-subtitle.tsx @@ -3,7 +3,7 @@ import { HTMLAttributes } from 'react'; import { subtitleVariants } from './insurance-subtitle.css'; interface InsuranceSubtitleProps extends HTMLAttributes { - name: string; // TODO ๋ช…์„ธ ํ•„๋“œ๋ช… ๋ฐ˜์˜ + name: string; type: keyof typeof DEFAULT_PLACEHOLDER; fontColor: 'primary500' | 'primary100'; fontStyle: 'm_16' | 'sb_14'; diff --git a/apps/client/src/shared/components/insurance-title/insurance-title.tsx b/apps/client/src/shared/components/insurance-title/insurance-title.tsx index f11cc5519..29f215ae5 100644 --- a/apps/client/src/shared/components/insurance-title/insurance-title.tsx +++ b/apps/client/src/shared/components/insurance-title/insurance-title.tsx @@ -1,8 +1,8 @@ import { titleVariants } from './insurance-title.css'; interface InsuranceTitleProps { - company?: string; // TODO ๋ช…์„ธ ํ•„๋“œ๋ช… ๋ฐ˜์˜ - name?: string; // TODO ๋ช…์„ธ ํ•„๋“œ๋ช… ๋ฐ˜์˜ + company?: string; + name?: string; fontColor: 'gray900' | 'white'; fontStyle: 'eb_24' | 'eb_28'; } diff --git a/apps/client/src/shared/hooks/use-toggle.ts b/apps/client/src/shared/hooks/use-toggle.ts new file mode 100644 index 000000000..c06db6b30 --- /dev/null +++ b/apps/client/src/shared/hooks/use-toggle.ts @@ -0,0 +1,31 @@ +import { useReducer } from 'react'; + +/** + * @description + * `useToggle`์€ boolean ์ƒํƒœ๋ฅผ ๊ฐ„๋‹จํ•˜๊ฒŒ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ฃผ๋Š” React ํ›…์ž…๋‹ˆ๋‹ค. + * ์ƒํƒœ๋ฅผ `true`์™€ `false` ์‚ฌ์ด์—์„œ ํ† ๊ธ€ํ•  ์ˆ˜ ์žˆ๋Š” ํ•จ์ˆ˜๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * + * @param {boolean} [initialValue=false] - ์ดˆ๊ธฐ ์ƒํƒœ ๊ฐ’. ๊ธฐ๋ณธ๊ฐ’์€ `false`์ž…๋‹ˆ๋‹ค. + * + * @returns {[state: boolean, toggle: () => void]} ํŠœํ”Œ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค: + * - state `boolean` - ํ˜„์žฌ ์ƒํƒœ ๊ฐ’ + * - toggle `() => void` - ์ƒํƒœ๋ฅผ ๋ฐ˜์ „์‹œํ‚ค๋Š” ํ•จ์ˆ˜ + * + * @example + * + * function Component() { + * const [open, toggle] = useToggle(false); + * + * return ( + *
+ *

Bottom Sheet ์ƒํƒœ: {open ? '์—ด๋ฆผ' : '๋‹ซํž˜'}

+ * + *
+ * ); + * } + */ +export function useToggle(initialValue = false) { + return useReducer(toggle, initialValue); +} + +const toggle = (state: boolean) => !state; diff --git a/apps/client/src/shared/hooks/use-user-info-valid.ts b/apps/client/src/shared/hooks/use-user-info-valid.ts deleted file mode 100644 index 741ce2b58..000000000 --- a/apps/client/src/shared/hooks/use-user-info-valid.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { UserInfoStateProps } from '@widgets/onboarding/type/user-info.type'; - -const isValidName = (name: string) => { - const trimmed = name.trim(); - const isCorrectLength = trimmed.length >= 2; - return isCorrectLength; -}; - -const isValidBirth = (year: string, month: string, day: string) => { - if (year.length !== 4) { - return false; - } - - const dateStr = `${year}-${month}-${day}`; - const date = new Date(dateStr); - const minDate = new Date('1900-01-01'); - const maxDate = new Date(); - - return ( - !isNaN(date.getTime()) && - date >= minDate && - date <= maxDate && - date.getFullYear() === Number(year) && - date.getMonth() + 1 === Number(month) && - date.getDate() === Number(day) - ); -}; - -export function useUserInfoValid(userInfo: UserInfoStateProps): boolean { - const { name, birthYear, birthMonth, birthDay, gender, occupation } = - userInfo; - - const isOccupationValid = - typeof occupation === 'string' && occupation.trim() !== ''; - - const validations = [ - isValidName(name), - isValidBirth(birthYear, birthMonth, birthDay), - gender !== null, - isOccupationValid, - ]; - - return validations.every(Boolean); -} diff --git a/apps/client/src/shared/router/path.ts b/apps/client/src/shared/router/path.ts index 8bee7409d..30c5ff39d 100644 --- a/apps/client/src/shared/router/path.ts +++ b/apps/client/src/shared/router/path.ts @@ -4,6 +4,7 @@ export const routePath = { COMMUNITY_WRITE: '/community/write', COMMUNITY_EDIT: '/community/edit/:postId', COMMUNITY_DETAIL: '/community/detail/:postId', + COMMUNITY_SEARCH: `/community/search`, LOGIN: '/login', ONBOARDING: '/onboarding', MY: '/my', diff --git a/apps/client/src/shared/router/routes/global-routes.tsx b/apps/client/src/shared/router/routes/global-routes.tsx index 574db3d48..6ebb6f933 100644 --- a/apps/client/src/shared/router/routes/global-routes.tsx +++ b/apps/client/src/shared/router/routes/global-routes.tsx @@ -1,4 +1,5 @@ import CommunityEdit from '@pages/community/community-edit/community-edit'; +import CommunitySearch from '@pages/community/community-search/community-search'; import SplashPage from '@pages/splash/splash-page.tsx'; import { @@ -47,6 +48,10 @@ export const protectedRoutes = [ path: routePath.COMMUNITY_EDIT, Component: CommunityEdit, }, + { + path: routePath.COMMUNITY_SEARCH, + Component: CommunitySearch, + }, { path: routePath.COMMUNITY_DETAIL, Component: CommunityDetail, diff --git a/apps/client/src/shared/types/schema.d.ts b/apps/client/src/shared/types/schema.d.ts index d62c02b72..d781334de 100644 --- a/apps/client/src/shared/types/schema.d.ts +++ b/apps/client/src/shared/types/schema.d.ts @@ -104,6 +104,30 @@ export interface paths { patch?: never; trace?: never; }; + '/posts/{post-id}/comments/{comment-id}/reply': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ๋Œ€๋Œ“๊ธ€ ์กฐํšŒ + * @description ์ปค๋ฎค๋‹ˆํ‹ฐ ๋Œ“๊ธ€์˜ ๋Œ€๋Œ“๊ธ€์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + */ + get: operations['getCommentReply']; + put?: never; + /** + * ๋Œ€๋Œ“๊ธ€ ์ž‘์„ฑ + * @description ์œ ์ €๊ฐ€ ์ปค๋ฎค๋‹ˆํ‹ฐ ๋Œ“๊ธ€์— ๋Œ€๋Œ“๊ธ€์„ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + post: operations['createCommentReply']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; '/oauth/reissue': { parameters: { query?: never; @@ -202,6 +226,74 @@ export interface paths { patch?: never; trace?: never; }; + '/users': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** + * ์œ ์ € ํ”„๋กœํ•„ ์ˆ˜์ • + * @description ์œ ์ €์˜ ํ”„๋กœํ•„์„ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค. + */ + patch: operations['updateProfile']; + trace?: never; + }; + '/posts/{post-id}/comments/{comment-id}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** + * ๋Œ“๊ธ€ ์‚ญ์ œ + * @description ์ปค๋ฎค๋‹ˆํ‹ฐ ๊ฒŒ์‹œ๊ธ€์˜ ๋Œ“๊ธ€์„ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค. + */ + delete: operations['deleteComment']; + options?: never; + head?: never; + /** + * ๋Œ“๊ธ€ ์ˆ˜์ • + * @description ์ปค๋ฎค๋‹ˆํ‹ฐ ๊ฒŒ์‹œ๊ธ€์˜ ๋Œ“๊ธ€์„ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค. + */ + patch: operations['updateComment']; + trace?: never; + }; + '/posts/{post-id}/comments/{comment-id}/reply/{comment-reply-id}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** + * ๋Œ€๋Œ“๊ธ€ ์‚ญ์ œ + * @description ์œ ์ €๊ฐ€ ์ปค๋ฎค๋‹ˆํ‹ฐ ๋Œ“๊ธ€์˜ ๋Œ€๋Œ“๊ธ€์„ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค. + */ + delete: operations['deleteCommentReply']; + options?: never; + head?: never; + /** + * ๋Œ€๋Œ“๊ธ€ ์ˆ˜์ • + * @description ์œ ์ €๊ฐ€ ์ปค๋ฎค๋‹ˆํ‹ฐ ๋Œ“๊ธ€์˜ ๋Œ€๋Œ“๊ธ€์„ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค. + */ + patch: operations['updateCommentReply']; + trace?: never; + }; '/users/me/report-summary': { parameters: { query?: never; @@ -302,6 +394,26 @@ export interface paths { patch?: never; trace?: never; }; + '/user-infos/insurances/options': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ๋ณดํ—˜ ์ถ”์ฒœ ์‹œ ์„ ํƒ ์‚ฌํ•ญ ํ•ญ๋ชฉ ์กฐํšŒ + * @description ๋ณดํ—˜ ์ถ”์ฒœ ์‹œ ๋ณดํ—˜ ์ƒํ’ˆ์˜ ์„ ํƒ ์‚ฌํ•ญ ํ•ญ๋ชฉ์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + */ + get: operations['getInsurancesOptions']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; '/user-infos/diagnosed-disease': { parameters: { query?: never; @@ -342,6 +454,46 @@ export interface paths { patch?: never; trace?: never; }; + '/posts/trend': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ์ธ๊ธฐ ๊ฒŒ์‹œ๋ฌผ ๋ชฉ๋ก ์กฐํšŒ + * @description ์ปค๋ฎค๋‹ˆํ‹ฐ์—์„œ ์ธ๊ธฐ๊ธ€ ๋ชฉ๋ก์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + */ + get: operations['getTrendPosts']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/posts/search': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * ๊ฒŒ์‹œ๋ฌผ ๊ฒ€์ƒ‰ + * @description ๊ฒ€์ƒ‰ ํ‚ค์›Œ๋“œ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ฒŒ์‹œ๋ฌผ์„ ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค. + */ + get: operations['searchPosts']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; '/insurances/reports/{insurance-report-id}': { parameters: { query?: never; @@ -462,7 +614,7 @@ export interface paths { patch?: never; trace?: never; }; - '/posts/{post-id}/comments/{comment-id}': { + '/oauth/kakao/unlink': { parameters: { query?: never; header?: never; @@ -472,11 +624,8 @@ export interface paths { get?: never; put?: never; post?: never; - /** - * ๋Œ“๊ธ€ ์‚ญ์ œ - * @description ์ปค๋ฎค๋‹ˆํ‹ฐ ๊ฒŒ์‹œ๊ธ€์˜ ๋Œ“๊ธ€์„ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค. - */ - delete: operations['deleteComment']; + /** ํšŒ์› ํƒˆํ‡ด */ + delete: operations['unlink']; options?: never; head?: never; patch?: never; @@ -486,17 +635,41 @@ export interface paths { export type webhooks = Record; export interface components { schemas: { - PostCreateRequest: { + PostUpdateRequest: { /** - * @description ๊ธ€ ์ œ๋ชฉ - * @example ์•„๋‹ˆ + * @description ์ œ๋ชฉ + * @example ใ…‡ใ…‡ */ - title: string; + newTitle: string; /** * @description ๋‚ด์šฉ - * @example ์ €ํฌ ๋Œ€์ƒํƒ€๋ฉด ์–ด๋–กํ•˜๋‚˜์š” ใ…ˆใ…‰๋กœ? + * @example ใ…‡ใ…‡ใ…‡ */ - content: string; + newContent: string; + /** + * @description ์นดํ…Œ๊ณ ๋ฆฌ + * @example INFORMATION + */ + newCategory: string; + /** @description ์ˆ˜์ •๋œ ์ด๋ฏธ์ง€ ๋ชฉ๋ก */ + updatedImages: components['schemas']['UpdateImageRequest'][]; + /** @description ์‚ญ์ œํ•  ์ด๋ฏธ์ง€ ID ๋ชฉ๋ก */ + deleteImageIds: number[]; + }; + /** @description ์ˆ˜์ •๋œ ์ด๋ฏธ์ง€ ๋ชฉ๋ก */ + UpdateImageRequest: { + /** + * Format: int64 + * @description ์ด๋ฏธ์ง€ ID + */ + id?: number; + /** @description ์ƒˆ ์ด๋ฏธ์ง€ URL (๋ณ€๊ฒฝ ์—†์œผ๋ฉด null) */ + imageUrl?: string; + /** + * Format: int32 + * @description ์ˆœ์„œ, 1๋ถ€ํ„ฐ ์‹œ์ž‘ + */ + sequence?: number; }; BaseResponsePostCreateResponse: { /** @@ -514,6 +687,25 @@ export interface components { */ postId?: number; }; + PostCreateRequest: { + /** + * @description ๊ธ€ ์ œ๋ชฉ + * @example ์•„๋‹ˆ + */ + title: string; + /** + * @description ๋‚ด์šฉ + * @example ์ €ํฌ ๋Œ€์ƒํƒ€๋ฉด ์–ด๋–กํ•˜๋‚˜์š” ใ…ˆใ…‰๋กœ? + */ + content: string; + /** + * @description ์นดํ…Œ๊ณ ๋ฆฌ + * @example QNA + */ + category: string; + /** @description ์ด๋ฏธ์ง€ url */ + imageUrls?: string[]; + }; BaseResponseVoid: { /** * Format: int32 @@ -529,6 +721,17 @@ export interface components { * @example ์ข‹์€ ๊ธ€์ด๋„ค์š” */ content: string; + /** @description ์ด๋ฏธ์ง€ url ๋ชฉ๋ก. url ์ด ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ์—๋„ ๋นˆ ๋ฆฌ์ŠคํŠธ๋ฅผ ์š”๊ตฌ */ + imageUrls: string[]; + }; + CommentReplyCreateRequest: { + /** + * @description ๋ณธ๋ฌธ + * @example ๋Œ€๋Œ“๊ธ€ ์ž‘์„ฑํ•˜๊ธฐ ~ + */ + content: string; + /** @description ์ด๋ฏธ์ง€ url ๋ชฉ๋ก. url ์ด ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ์—๋„ ๋นˆ ๋ฆฌ์ŠคํŠธ๋ฅผ ์š”๊ตฌ */ + imageUrls: string[]; }; BaseResponseTokenReissueResponse: { /** @@ -687,6 +890,26 @@ export interface components { * @example 150000 */ maxPremium: number; + /** + * @description ๋‚ฉ์ž…๊ตฌ์กฐ + * @enum {string} + */ + renewableType?: 'RENEWABLE' | 'NON_RENEWABLE'; + /** + * @description ํ™˜๊ธ‰๊ตฌ์กฐ + * @enum {string} + */ + refundType?: 'PROTECTION_ONLY' | 'PARTIAL_RETURN' | 'FULL_RETURN'; + /** + * @description ๋‚ฉ๋ถ€๊ธฐ๊ฐ„ + * @enum {string} + */ + paymentPeriod?: 'YEAR_10' | 'YEAR_20' | 'YEAR_30'; + /** + * @description ๋งŒ๊ธฐ + * @enum {string} + */ + maturityAge?: 'OLD_80' | 'OLD_90' | 'OLD_100'; }; BaseResponseIssueInsuranceReportResponse: { /** @@ -717,6 +940,37 @@ export interface components { PresignedUrlResponse: { presignedUrls?: string[]; }; + UserUpdateProfileRequest: { + /** + * @description ๋‹‰๋„ค์ž„ + * @example ๊น€์žฌํ—Œ + */ + nickname?: string; + /** @description ํ”„๋กœํ•„ ์ด๋ฏธ์ง€ url */ + profileImageUrl?: string; + }; + CommentUpdateRequest: { + /** + * @description ์ˆ˜์ •ํ•  ๋Œ“๊ธ€ ๋‚ด์šฉ + * @example ์ข‹์€ ๊ธ€์ด๋„ค์š” + */ + content?: string; + /** @description ์ˆ˜์ •๋œ ์ด๋ฏธ์ง€ ๋ชฉ๋ก */ + updatedImages: components['schemas']['UpdateImageRequest'][]; + /** @description ์‚ญ์ œํ•  ์ด๋ฏธ์ง€ ID ๋ชฉ๋ก */ + deleteImageIds: number[]; + }; + CommentReplyUpdateRequest: { + /** + * @description ์ˆ˜์ •ํ•  ๋Œ€๋Œ“๊ธ€ ๋‚ด์šฉ + * @example ์ข‹์€ ๋Œ“๊ธ€์ด๋„ค์š” + */ + content?: string; + /** @description ์ˆ˜์ •๋œ ์ด๋ฏธ์ง€ ๋ชฉ๋ก */ + updatedImages: components['schemas']['UpdateImageRequest'][]; + /** @description ์‚ญ์ œํ•  ์ด๋ฏธ์ง€ ID ๋ชฉ๋ก */ + deleteImageIds: number[]; + }; BaseResponseInsuranceReportSummaryResponse: { /** * Format: int32 @@ -775,8 +1029,13 @@ export interface components { * @description ์ž‘์„ฑ ์‹œ๊ฐ„ */ createdAt?: string; - /** Format: int64 */ - cursor?: number; + /** + * Format: int32 + * @description ์ข‹์•„์š” ์ˆ˜ + */ + likeCount?: number; + /** @description ์‚ฌ์šฉ์ž ์ข‹์•„์š” ์—ฌ๋ถ€ */ + isLike?: boolean; }; SliceResponseMyPostSummaryResponseLong: { /** @description ๋ฐ์ดํ„ฐ ๋ชฉ๋ก */ @@ -820,8 +1079,6 @@ export interface components { * @description ์ž‘์„ฑ ์‹œ๊ฐ„ */ createdAt?: string; - /** Format: int64 */ - cursor?: number; }; SliceResponseMyCommentSummaryResponseLong: { /** @description ๋ฐ์ดํ„ฐ ๋ชฉ๋ก */ @@ -893,6 +1150,41 @@ export interface components { JobResponses: { jobs?: components['schemas']['JobResponse'][]; }; + BaseResponseInsuranceOptionsResponse: { + /** + * Format: int32 + * @example 200 + */ + code?: number; + message?: string; + data?: components['schemas']['InsuranceOptionsResponse']; + }; + InsuranceOptionsResponse: { + renewableTypes?: components['schemas']['RenewableTypeResponse'][]; + refundTypes?: components['schemas']['RefundTypeResponse'][]; + paymentPeriods?: components['schemas']['PaymentPeriodResponse'][]; + maturityAges?: components['schemas']['MaturityAgeResponse'][]; + }; + MaturityAgeResponse: { + /** @enum {string} */ + maturityAge?: 'OLD_80' | 'OLD_90' | 'OLD_100'; + displayName?: string; + }; + PaymentPeriodResponse: { + /** @enum {string} */ + paymentPeriod?: 'YEAR_10' | 'YEAR_20' | 'YEAR_30'; + displayName?: string; + }; + RefundTypeResponse: { + /** @enum {string} */ + refundType?: 'PROTECTION_ONLY' | 'PARTIAL_RETURN' | 'FULL_RETURN'; + displayName?: string; + }; + RenewableTypeResponse: { + /** @enum {string} */ + renewableType?: 'RENEWABLE' | 'NON_RENEWABLE'; + displayName?: string; + }; BaseResponseDiagnosedDiseaseResponses: { /** * Format: int32 @@ -993,8 +1285,13 @@ export interface components { * @description ๊ฒŒ์‹œ๋ฌผ ์ž‘์„ฑ ์‹œ๊ฐ */ createdAt?: string; - /** Format: int64 */ - cursor?: number; + /** + * Format: int32 + * @description ์ข‹์•„์š” ์ˆ˜ + */ + likeCount?: number; + /** @description ์‚ฌ์šฉ์ž์˜ ์ข‹์•„์š” ์—ฌ๋ถ€ */ + likedByCurrentUser?: boolean; }; SliceResponsePostSummaryResponseLong: { /** @description ๋ฐ์ดํ„ฐ ๋ชฉ๋ก */ @@ -1016,6 +1313,22 @@ export interface components { message?: string; data?: components['schemas']['PostDetailResponse']; }; + /** @description ๊ฒŒ์‹œ๋ฌผ ์นดํ…Œ๊ณ ๋ฆฌ */ + PostCategoryResponse: { + /** @enum {string} */ + category?: 'QNA' | 'INFORMATION' | 'CONVERSATION'; + description?: string; + }; + /** @description ์ด๋ฏธ์ง€ url */ + PostDetailImageResponse: { + /** + * Format: int64 + * @description ์ด๋ฏธ์ง€ ID + */ + imageId?: number; + /** @description ์ด๋ฏธ์ง€ url */ + imageUrl?: string; + }; PostDetailResponse: { /** * Format: int64 @@ -1034,7 +1347,7 @@ export interface components { /** @description ๊ฒŒ์‹œ๋ฌผ ๋‚ด์šฉ */ content?: string; /** - * Format: int64 + * Format: int32 * @description ๋Œ“๊ธ€ ์ˆ˜ * @example 8 */ @@ -1044,21 +1357,44 @@ export interface components { * @description ์ƒ์„ฑ ์‹œ๊ฐ„ */ createdAt?: string; - }; - BaseResponseSliceResponseCommentResponseLong: { + /** + * Format: date-time + * @description ์ˆ˜์ • ์‹œ๊ฐ„ + */ + updatedAt?: string; + /** + * Format: int32 + * @description ์ข‹์•„์š” ์ˆ˜ + */ + likeCount?: number; + /** @description ์‚ฌ์šฉ์ž ์ข‹์•„์š” ์—ฌ๋ถ€ */ + likedByCurrentUser?: boolean; + category?: components['schemas']['PostCategoryResponse']; + /** @description ์ด๋ฏธ์ง€ url */ + imageUrl?: components['schemas']['PostDetailImageResponse'][]; + }; + BaseResponseSliceResponseCommentWithImagesResponseLong: { /** * Format: int32 * @example 200 */ code?: number; message?: string; - data?: components['schemas']['SliceResponseCommentResponseLong']; + data?: components['schemas']['SliceResponseCommentWithImagesResponseLong']; + }; + /** @description ๋Œ“๊ธ€ ์ด๋ฏธ์ง€ ๋ชฉ๋ก */ + CommentImageResponse: { + /** Format: int64 */ + imageId?: number; + imageUrl?: string; + /** Format: int32 */ + sequence?: number; }; /** @description ๋ฐ์ดํ„ฐ ๋ชฉ๋ก */ - CommentResponse: { + CommentWithImagesResponse: { /** * Format: int64 - * @description ๊ฒŒ์‹œ๊ธ€ id + * @description ๋Œ“๊ธ€ ID */ commentId?: number; /** @@ -1072,6 +1408,11 @@ export interface components { profileImage?: string; /** @description ๋Œ“๊ธ€ ๋‚ด์šฉ */ content?: string; + /** + * Format: int32 + * @description ๋Œ€๋Œ“๊ธ€ ๊ฐœ์ˆ˜ + */ + replyCount?: number; /** * Format: date-time * @description ์ƒ์„ฑ ์‹œ๊ฐ„ @@ -1082,12 +1423,12 @@ export interface components { * @description ์ˆ˜์ • ์‹œ๊ฐ„ */ updatedAt?: string; - /** Format: int64 */ - cursor?: number; + /** @description ๋Œ“๊ธ€ ์ด๋ฏธ์ง€ ๋ชฉ๋ก */ + images?: components['schemas']['CommentImageResponse'][]; }; - SliceResponseCommentResponseLong: { + SliceResponseCommentWithImagesResponseLong: { /** @description ๋ฐ์ดํ„ฐ ๋ชฉ๋ก */ - content?: components['schemas']['CommentResponse'][]; + content?: components['schemas']['CommentWithImagesResponse'][]; /** * Format: int64 * @description ๋‹ค์Œ ์ปค์„œ @@ -1096,35 +1437,217 @@ export interface components { /** @description ๋งˆ์ง€๋ง‰ ํŽ˜์ด์ง€ ์—ฌ๋ถ€ */ isLast?: boolean; }; - BaseResponseInsuranceReportResponse: { + BaseResponseSliceResponseCommentReplyWithImagesResponseLong: { /** * Format: int32 * @example 200 */ code?: number; message?: string; - data?: components['schemas']['InsuranceReportResponse']; + data?: components['schemas']['SliceResponseCommentReplyWithImagesResponseLong']; }; - BasicInformation: { - name?: string; - company?: string; - productType?: string; - /** Format: int32 */ - minEnrollmentAge?: number; - /** Format: int32 */ - maxEnrollmentAge?: number; - /** Format: int32 */ - premium?: number; - /** Format: int32 */ - maturityAge?: number; + /** @description ๋Œ€๋Œ“๊ธ€ ์ด๋ฏธ์ง€ ๋ชฉ๋ก */ + CommentReplyImageResponse: { + /** Format: int64 */ + commentReplyImageId?: number; + imageUrl?: string; /** Format: int32 */ - paymentPeriodYears?: number; + sequence?: number; }; - InsuranceReportResponse: { + /** @description ๋ฐ์ดํ„ฐ ๋ชฉ๋ก */ + CommentReplyWithImagesResponse: { + /** + * Format: int64 + * @description ๋Œ€๋Œ“๊ธ€ ID + */ + commentReplyId?: number; + /** + * Format: int64 + * @description ์ž‘์„ฑ์ž ID + */ + writerId?: number; + /** @description ์ž‘์„ฑ์ž ๋‹‰๋„ค์ž„ */ + writerNickname?: string; + /** @description ์ž‘์„ฑ์ž ํ”„๋กœํ•„ ์ด๋ฏธ์ง€ */ + profileImage?: string; + /** @description ๋Œ“๊ธ€ ๋‚ด์šฉ */ + content?: string; + /** + * Format: date-time + * @description ์ƒ์„ฑ ์‹œ๊ฐ„ + */ + createdAt?: string; + /** + * Format: date-time + * @description ์ˆ˜์ • ์‹œ๊ฐ„ + */ + updatedAt?: string; + /** @description ๋Œ€๋Œ“๊ธ€ ์ด๋ฏธ์ง€ ๋ชฉ๋ก */ + images?: components['schemas']['CommentReplyImageResponse'][]; + }; + SliceResponseCommentReplyWithImagesResponseLong: { + /** @description ๋ฐ์ดํ„ฐ ๋ชฉ๋ก */ + content?: components['schemas']['CommentReplyWithImagesResponse'][]; + /** + * Format: int64 + * @description ๋‹ค์Œ ์ปค์„œ + */ + nextCursor?: number; + /** @description ๋งˆ์ง€๋ง‰ ํŽ˜์ด์ง€ ์—ฌ๋ถ€ */ + isLast?: boolean; + }; + BaseResponseTrendingPostsResponses: { + /** + * Format: int32 + * @example 200 + */ + code?: number; + message?: string; + data?: components['schemas']['TrendingPostsResponses']; + }; + TrendingPostsResponse: { + /** + * Format: int64 + * @description ๊ฒŒ์‹œ๊ธ€ ID + */ + postId?: number; + /** + * Format: int64 + * @description ์ž‘์„ฑ์ž ID + */ + writerId?: number; + /** + * @description ๋‹‰๋„ค์ž„ + * @example ์ •ํ›ˆ ์žฅ + */ + writerNickname?: string; + /** @description ๊ฒŒ์‹œ๋ฌผ ์ œ๋ชฉ */ + title?: string; + /** @description ๊ฒŒ์‹œ๋ฌผ ๋‚ด์šฉ */ + content?: string; + /** + * Format: int32 + * @description ๋Œ“๊ธ€ ์ˆ˜ + * @example 8 + */ + commentCount?: number; + /** + * Format: date-time + * @description ์ƒ์„ฑ ์‹œ๊ฐ„ + */ + createdAt?: string; + /** + * Format: int32 + * @description ์ข‹์•„์š” ์ˆ˜ + */ + likeCount?: number; + /** @description ์‚ฌ์šฉ์ž ์ข‹์•„์š” ์—ฌ๋ถ€ */ + likedByCurrentUser?: boolean; + category?: components['schemas']['PostCategoryResponse']; + }; + TrendingPostsResponses: { + posts?: components['schemas']['TrendingPostsResponse'][]; + }; + BaseResponseSliceResponsePostSearchResponseString: { + /** + * Format: int32 + * @example 200 + */ + code?: number; + message?: string; + data?: components['schemas']['SliceResponsePostSearchResponseString']; + }; + /** @description ๋ฐ์ดํ„ฐ ๋ชฉ๋ก */ + PostSearchResponse: { + /** + * Format: int64 + * @description ๊ฒŒ์‹œ๊ธ€ ID + */ + postId?: number; + /** + * Format: int64 + * @description ์ž‘์„ฑ์ž ID + */ + writerId?: number; + /** + * @description ๊ฒŒ์‹œ๊ธ€ ์ œ๋ชฉ + * @example ์•„๋‹ˆ + */ + title?: string; + /** + * @description ๊ฒŒ์‹œ๊ธ€ ๋‚ด์šฉ + * @example ์žฅ์ •ํ›ˆ ๊ทธ๋Š” ๋ˆ„๊ตฌ์ธ๊ฐ€ + */ + content?: string; + /** + * @description ์ž‘์„ฑ์ž ๋‹‰๋„ค์ž„ + * @example ์ •ํ›ˆ ์žฅ + */ + writerNickname?: string; + /** @description ์ž‘์„ฑ์ž ํ”„๋กœํ•„ ์‚ฌ์ง„ */ + profileImageUrl?: string; + /** + * Format: int32 + * @description ๋Œ“๊ธ€ ์ˆ˜ + * @example 8 + */ + commentCount?: number; + /** + * Format: date-time + * @description ๊ฒŒ์‹œ๋ฌผ ์ž‘์„ฑ ์‹œ๊ฐ + */ + createdAt?: string; + /** + * Format: int32 + * @description ์ข‹์•„์š” ์ˆ˜ + */ + likeCount?: number; + /** @description ์‚ฌ์šฉ์ž์˜ ์ข‹์•„์š” ์—ฌ๋ถ€ */ + likedByCurrentUser?: boolean; + /** + * Format: double + * @description ์—ฐ๊ด€๋„ + */ + relevanceScore?: number; + }; + SliceResponsePostSearchResponseString: { + /** @description ๋ฐ์ดํ„ฐ ๋ชฉ๋ก */ + content?: components['schemas']['PostSearchResponse'][]; + /** @description ๋‹ค์Œ ์ปค์„œ */ + nextCursor?: string; + /** @description ๋งˆ์ง€๋ง‰ ํŽ˜์ด์ง€ ์—ฌ๋ถ€ */ + isLast?: boolean; + }; + BaseResponseInsuranceReportResponse: { + /** + * Format: int32 + * @example 200 + */ + code?: number; + message?: string; + data?: components['schemas']['InsuranceReportResponse']; + }; + BasicInformationResponse: { + name?: string; + company?: string; + productType?: string; + /** Format: int32 */ + minEnrollmentAge?: number; + /** Format: int32 */ + maxEnrollmentAge?: number; + /** Format: int32 */ + premium?: number; + /** Format: int32 */ + maturityAge?: number; + paymentPeriod?: components['schemas']['PaymentPeriodResponse']; + renewableType?: components['schemas']['RenewableTypeResponse']; + refundType?: components['schemas']['RefundTypeResponse']; + }; + InsuranceReportResponse: { /** Format: uuid */ reportId?: string; - reportInformation?: components['schemas']['BasicInformation']; - reportRationale?: components['schemas']['ReportRationale']; + reportInformation?: components['schemas']['BasicInformationResponse']; + reportRationale?: components['schemas']['ReportRationaleResponse']; majorDisease?: components['schemas']['SectionData']; surgery?: components['schemas']['SectionData']; hospitalization?: components['schemas']['SectionData']; @@ -1132,7 +1655,7 @@ export interface components { death?: components['schemas']['SectionData']; externalUri?: string; }; - ReportRationale: { + ReportRationaleResponse: { reasons?: string[]; keywordChips?: string[]; }; @@ -1326,7 +1849,7 @@ export interface operations { }; requestBody: { content: { - 'application/json': components['schemas']['PostCreateRequest']; + 'application/json': components['schemas']['PostUpdateRequest']; }; }; responses: { @@ -1462,6 +1985,8 @@ export interface operations { getAllPosts: { parameters: { query?: { + sort?: 'LATEST' | 'POPULAR'; + category?: 'ALL' | 'QNA' | 'INFORMATION' | 'CONVERSATION'; cursor?: number; size?: number; }; @@ -1545,7 +2070,752 @@ export interface operations { }; requestBody: { content: { - 'application/json': components['schemas']['PostCreateRequest']; + 'application/json': components['schemas']['PostCreateRequest']; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + '*/*': components['schemas']['BaseResponsePostCreateResponse']; + }; + }; + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + 405: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + }; + }; + createPostLike: { + parameters: { + query?: never; + header?: never; + path: { + 'post-id': number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + '*/*': components['schemas']['BaseResponseVoid']; + }; + }; + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + 405: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + 409: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + }; + }; + deletePostLike: { + parameters: { + query?: never; + header?: never; + path: { + 'post-id': number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + '*/*': components['schemas']['BaseResponseVoid']; + }; + }; + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + 405: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + }; + }; + getComments: { + parameters: { + query?: { + cursor?: number; + size?: number; + }; + header?: never; + path: { + 'post-id': number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + '*/*': components['schemas']['BaseResponseSliceResponseCommentWithImagesResponseLong']; + }; + }; + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + 405: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + 409: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + }; + }; + createComment: { + parameters: { + query?: never; + header?: never; + path: { + 'post-id': number; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['CommentCreateRequest']; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + '*/*': components['schemas']['BaseResponsePostCreateResponse']; + }; + }; + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + 405: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + }; + }; + getCommentReply: { + parameters: { + query?: { + cursor?: number; + size?: number; + }; + header?: never; + path: { + 'post-id': number; + 'comment-id': number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + '*/*': components['schemas']['BaseResponseSliceResponseCommentReplyWithImagesResponseLong']; + }; + }; + /** @description ๊ฒฝ๋กœ ๋ณ€์ˆ˜ ๊ฐ’์ด ๋ˆ„๋ฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + /** @description ์œ ํšจํ•˜์ง€ ์•Š์€ JWT์ž…๋‹ˆ๋‹ค. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + /** @description ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค. */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + /** @description ์ง€์›ํ•˜์ง€ ์•Š๋Š” URL์ž…๋‹ˆ๋‹ค. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + /** @description ์œ ํšจํ•˜์ง€ ์•Š์€ Http ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค. */ + 405: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + /** @description ์™ธ๋ถ€ ์„œ๋ฒ„ ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + }; + }; + createCommentReply: { + parameters: { + query?: never; + header?: never; + path: { + 'post-id': number; + 'comment-id': number; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['CommentReplyCreateRequest']; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + '*/*': components['schemas']['BaseResponseVoid']; + }; + }; + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + 405: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + }; + }; + reissue: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + '*/*': components['schemas']['BaseResponseTokenReissueResponse']; + }; + }; + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + 405: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + }; + }; + logout: { + parameters: { + query?: { + 'redirect-url'?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + '*/*': components['schemas']['BaseResponseString']; + }; + }; + /** @description ๊ฒฝ๋กœ ๋ณ€์ˆ˜ ๊ฐ’์ด ๋ˆ„๋ฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + /** @description ์œ ํšจํ•˜์ง€ ์•Š์€ JWT์ž…๋‹ˆ๋‹ค. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + /** @description ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค. */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + /** @description ์ง€์›ํ•˜์ง€ ์•Š๋Š” URL์ž…๋‹ˆ๋‹ค. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + /** @description ์œ ํšจํ•˜์ง€ ์•Š์€ Http ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค. */ + 405: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + /** @description ์™ธ๋ถ€ ์„œ๋ฒ„ ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + }; + }; + kakaoCallback_1: { + parameters: { + query: { + code: string; + 'redirect-url'?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + '*/*': components['schemas']['BaseResponseKaKaoLoginResponse']; + }; + }; + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + 405: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + }; + }; + kakaoCallback: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['OAuthLoginRequest']; }; }; responses: { @@ -1555,7 +2825,7 @@ export interface operations { [name: string]: unknown; }; content: { - '*/*': components['schemas']['BaseResponsePostCreateResponse']; + '*/*': components['schemas']['BaseResponseKaKaoLoginResponse']; }; }; 400: { @@ -1608,16 +2878,18 @@ export interface operations { }; }; }; - createPostLike: { + issueReport: { parameters: { query?: never; header?: never; - path: { - 'post-id': number; - }; + path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + 'application/json': components['schemas']['InsuranceReportRequest']; + }; + }; responses: { /** @description OK */ 200: { @@ -1625,7 +2897,7 @@ export interface operations { [name: string]: unknown; }; content: { - '*/*': components['schemas']['BaseResponseVoid']; + '*/*': components['schemas']['BaseResponseIssueInsuranceReportResponse']; }; }; 400: { @@ -1668,14 +2940,6 @@ export interface operations { 'application/json': unknown; }; }; - 409: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': unknown; - }; - }; 500: { headers: { [name: string]: unknown; @@ -1686,16 +2950,18 @@ export interface operations { }; }; }; - deletePostLike: { + createdUrls: { parameters: { query?: never; header?: never; - path: { - 'post-id': number; - }; + path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + 'application/json': components['schemas']['PresignedUrlRequest']; + }; + }; responses: { /** @description OK */ 200: { @@ -1703,7 +2969,7 @@ export interface operations { [name: string]: unknown; }; content: { - '*/*': components['schemas']['BaseResponseVoid']; + '*/*': components['schemas']['BaseResponsePresignedUrlResponse']; }; }; 400: { @@ -1756,19 +3022,18 @@ export interface operations { }; }; }; - getComments: { + updateProfile: { parameters: { - query?: { - cursor?: number; - size?: number; - }; + query?: never; header?: never; - path: { - 'post-id': number; - }; + path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + 'application/json': components['schemas']['UserUpdateProfileRequest']; + }; + }; responses: { /** @description OK */ 200: { @@ -1776,7 +3041,7 @@ export interface operations { [name: string]: unknown; }; content: { - '*/*': components['schemas']['BaseResponseSliceResponseCommentResponseLong']; + '*/*': components['schemas']['BaseResponseVoid']; }; }; 400: { @@ -1819,14 +3084,6 @@ export interface operations { 'application/json': unknown; }; }; - 409: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': unknown; - }; - }; 500: { headers: { [name: string]: unknown; @@ -1837,20 +3094,17 @@ export interface operations { }; }; }; - createComment: { + deleteComment: { parameters: { query?: never; header?: never; path: { 'post-id': number; + 'comment-id': number; }; cookie?: never; }; - requestBody: { - content: { - 'application/json': components['schemas']['CommentCreateRequest']; - }; - }; + requestBody?: never; responses: { /** @description OK */ 200: { @@ -1858,7 +3112,7 @@ export interface operations { [name: string]: unknown; }; content: { - '*/*': components['schemas']['BaseResponsePostCreateResponse']; + '*/*': components['schemas']['BaseResponseVoid']; }; }; 400: { @@ -1901,6 +3155,14 @@ export interface operations { 'application/json': unknown; }; }; + 409: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; 500: { headers: { [name: string]: unknown; @@ -1911,14 +3173,21 @@ export interface operations { }; }; }; - reissue: { + updateComment: { parameters: { query?: never; header?: never; - path?: never; + path: { + 'post-id': number; + 'comment-id': number; + }; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + 'application/json': components['schemas']['CommentUpdateRequest']; + }; + }; responses: { /** @description OK */ 200: { @@ -1926,7 +3195,7 @@ export interface operations { [name: string]: unknown; }; content: { - '*/*': components['schemas']['BaseResponseTokenReissueResponse']; + '*/*': components['schemas']['BaseResponsePostCreateResponse']; }; }; 400: { @@ -1979,13 +3248,15 @@ export interface operations { }; }; }; - logout: { + deleteCommentReply: { parameters: { - query?: { - 'redirect-url'?: string; - }; + query?: never; header?: never; - path?: never; + path: { + 'post-id': number; + 'comment-id': number; + 'comment-reply-id': number; + }; cookie?: never; }; requestBody?: never; @@ -1996,10 +3267,9 @@ export interface operations { [name: string]: unknown; }; content: { - '*/*': components['schemas']['BaseResponseString']; + '*/*': components['schemas']['BaseResponseVoid']; }; }; - /** @description ๊ฒฝ๋กœ ๋ณ€์ˆ˜ ๊ฐ’์ด ๋ˆ„๋ฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. */ 400: { headers: { [name: string]: unknown; @@ -2008,7 +3278,6 @@ export interface operations { 'application/json': unknown; }; }; - /** @description ์œ ํšจํ•˜์ง€ ์•Š์€ JWT์ž…๋‹ˆ๋‹ค. */ 401: { headers: { [name: string]: unknown; @@ -2017,7 +3286,6 @@ export interface operations { 'application/json': unknown; }; }; - /** @description ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค. */ 403: { headers: { [name: string]: unknown; @@ -2026,7 +3294,6 @@ export interface operations { 'application/json': unknown; }; }; - /** @description ์ง€์›ํ•˜์ง€ ์•Š๋Š” URL์ž…๋‹ˆ๋‹ค. */ 404: { headers: { [name: string]: unknown; @@ -2035,7 +3302,6 @@ export interface operations { 'application/json': unknown; }; }; - /** @description ์œ ํšจํ•˜์ง€ ์•Š์€ Http ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค. */ 405: { headers: { [name: string]: unknown; @@ -2044,7 +3310,6 @@ export interface operations { 'application/json': unknown; }; }; - /** @description ์™ธ๋ถ€ ์„œ๋ฒ„ ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. */ 500: { headers: { [name: string]: unknown; @@ -2055,17 +3320,22 @@ export interface operations { }; }; }; - kakaoCallback_1: { + updateCommentReply: { parameters: { - query: { - code: string; - 'redirect-url'?: string; - }; + query?: never; header?: never; - path?: never; + path: { + 'post-id': number; + 'comment-id': number; + 'comment-reply-id': number; + }; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + 'application/json': components['schemas']['CommentReplyUpdateRequest']; + }; + }; responses: { /** @description OK */ 200: { @@ -2073,7 +3343,7 @@ export interface operations { [name: string]: unknown; }; content: { - '*/*': components['schemas']['BaseResponseKaKaoLoginResponse']; + '*/*': components['schemas']['BaseResponseVoid']; }; }; 400: { @@ -2126,18 +3396,14 @@ export interface operations { }; }; }; - kakaoCallback: { + getMyLastInsuranceReportSummary: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - requestBody: { - content: { - 'application/json': components['schemas']['OAuthLoginRequest']; - }; - }; + requestBody?: never; responses: { /** @description OK */ 200: { @@ -2145,7 +3411,7 @@ export interface operations { [name: string]: unknown; }; content: { - '*/*': components['schemas']['BaseResponseKaKaoLoginResponse']; + '*/*': components['schemas']['BaseResponseInsuranceReportSummaryResponse']; }; }; 400: { @@ -2198,18 +3464,17 @@ export interface operations { }; }; }; - issueReport: { + getMyPosts: { parameters: { - query?: never; + query?: { + cursorId?: number; + size?: number; + }; header?: never; path?: never; cookie?: never; }; - requestBody: { - content: { - 'application/json': components['schemas']['InsuranceReportRequest']; - }; - }; + requestBody?: never; responses: { /** @description OK */ 200: { @@ -2217,7 +3482,7 @@ export interface operations { [name: string]: unknown; }; content: { - '*/*': components['schemas']['BaseResponseIssueInsuranceReportResponse']; + '*/*': components['schemas']['BaseResponseSliceResponseMyPostSummaryResponseLong']; }; }; 400: { @@ -2270,18 +3535,17 @@ export interface operations { }; }; }; - createdUrls: { + getMyComments: { parameters: { - query?: never; + query?: { + cursorId?: number; + size?: number; + }; header?: never; path?: never; cookie?: never; }; - requestBody: { - content: { - 'application/json': components['schemas']['PresignedUrlRequest']; - }; - }; + requestBody?: never; responses: { /** @description OK */ 200: { @@ -2289,7 +3553,7 @@ export interface operations { [name: string]: unknown; }; content: { - '*/*': components['schemas']['BaseResponsePresignedUrlResponse']; + '*/*': components['schemas']['BaseResponseSliceResponseMyCommentSummaryResponseLong']; }; }; 400: { @@ -2342,7 +3606,7 @@ export interface operations { }; }; }; - getMyLastInsuranceReportSummary: { + getInfo: { parameters: { query?: never; header?: never; @@ -2357,7 +3621,7 @@ export interface operations { [name: string]: unknown; }; content: { - '*/*': components['schemas']['BaseResponseInsuranceReportSummaryResponse']; + '*/*': components['schemas']['BaseResponseUserProfileResponse']; }; }; 400: { @@ -2410,12 +3674,9 @@ export interface operations { }; }; }; - getMyPosts: { + getJobs: { parameters: { - query?: { - cursorId?: number; - size?: number; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -2428,9 +3689,10 @@ export interface operations { [name: string]: unknown; }; content: { - '*/*': components['schemas']['BaseResponseSliceResponseMyPostSummaryResponseLong']; + '*/*': components['schemas']['BaseResponseJobResponses']; }; }; + /** @description ๊ฒฝ๋กœ ๋ณ€์ˆ˜ ๊ฐ’์ด ๋ˆ„๋ฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. */ 400: { headers: { [name: string]: unknown; @@ -2439,6 +3701,7 @@ export interface operations { 'application/json': unknown; }; }; + /** @description ์œ ํšจํ•˜์ง€ ์•Š์€ JWT์ž…๋‹ˆ๋‹ค. */ 401: { headers: { [name: string]: unknown; @@ -2447,6 +3710,7 @@ export interface operations { 'application/json': unknown; }; }; + /** @description ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค. */ 403: { headers: { [name: string]: unknown; @@ -2455,6 +3719,7 @@ export interface operations { 'application/json': unknown; }; }; + /** @description ์ง€์›ํ•˜์ง€ ์•Š๋Š” URL์ž…๋‹ˆ๋‹ค. */ 404: { headers: { [name: string]: unknown; @@ -2463,6 +3728,7 @@ export interface operations { 'application/json': unknown; }; }; + /** @description ์œ ํšจํ•˜์ง€ ์•Š์€ Http ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค. */ 405: { headers: { [name: string]: unknown; @@ -2471,6 +3737,7 @@ export interface operations { 'application/json': unknown; }; }; + /** @description ์™ธ๋ถ€ ์„œ๋ฒ„ ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. */ 500: { headers: { [name: string]: unknown; @@ -2481,12 +3748,9 @@ export interface operations { }; }; }; - getMyComments: { + getInsurancesOptions: { parameters: { - query?: { - cursorId?: number; - size?: number; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -2499,9 +3763,10 @@ export interface operations { [name: string]: unknown; }; content: { - '*/*': components['schemas']['BaseResponseSliceResponseMyCommentSummaryResponseLong']; + '*/*': components['schemas']['BaseResponseInsuranceOptionsResponse']; }; }; + /** @description ๊ฒฝ๋กœ ๋ณ€์ˆ˜ ๊ฐ’์ด ๋ˆ„๋ฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. */ 400: { headers: { [name: string]: unknown; @@ -2510,6 +3775,7 @@ export interface operations { 'application/json': unknown; }; }; + /** @description ์œ ํšจํ•˜์ง€ ์•Š์€ JWT์ž…๋‹ˆ๋‹ค. */ 401: { headers: { [name: string]: unknown; @@ -2518,6 +3784,7 @@ export interface operations { 'application/json': unknown; }; }; + /** @description ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค. */ 403: { headers: { [name: string]: unknown; @@ -2526,6 +3793,7 @@ export interface operations { 'application/json': unknown; }; }; + /** @description ์ง€์›ํ•˜์ง€ ์•Š๋Š” URL์ž…๋‹ˆ๋‹ค. */ 404: { headers: { [name: string]: unknown; @@ -2534,6 +3802,7 @@ export interface operations { 'application/json': unknown; }; }; + /** @description ์œ ํšจํ•˜์ง€ ์•Š์€ Http ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค. */ 405: { headers: { [name: string]: unknown; @@ -2542,6 +3811,7 @@ export interface operations { 'application/json': unknown; }; }; + /** @description ์™ธ๋ถ€ ์„œ๋ฒ„ ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. */ 500: { headers: { [name: string]: unknown; @@ -2552,7 +3822,7 @@ export interface operations { }; }; }; - getInfo: { + getDiagnosedDisease: { parameters: { query?: never; header?: never; @@ -2567,9 +3837,10 @@ export interface operations { [name: string]: unknown; }; content: { - '*/*': components['schemas']['BaseResponseUserProfileResponse']; + '*/*': components['schemas']['BaseResponseDiagnosedDiseaseResponses']; }; }; + /** @description ๊ฒฝ๋กœ ๋ณ€์ˆ˜ ๊ฐ’์ด ๋ˆ„๋ฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. */ 400: { headers: { [name: string]: unknown; @@ -2578,6 +3849,7 @@ export interface operations { 'application/json': unknown; }; }; + /** @description ์œ ํšจํ•˜์ง€ ์•Š์€ JWT์ž…๋‹ˆ๋‹ค. */ 401: { headers: { [name: string]: unknown; @@ -2586,6 +3858,7 @@ export interface operations { 'application/json': unknown; }; }; + /** @description ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค. */ 403: { headers: { [name: string]: unknown; @@ -2594,6 +3867,7 @@ export interface operations { 'application/json': unknown; }; }; + /** @description ์ง€์›ํ•˜์ง€ ์•Š๋Š” URL์ž…๋‹ˆ๋‹ค. */ 404: { headers: { [name: string]: unknown; @@ -2602,6 +3876,7 @@ export interface operations { 'application/json': unknown; }; }; + /** @description ์œ ํšจํ•˜์ง€ ์•Š์€ Http ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค. */ 405: { headers: { [name: string]: unknown; @@ -2610,6 +3885,7 @@ export interface operations { 'application/json': unknown; }; }; + /** @description ์™ธ๋ถ€ ์„œ๋ฒ„ ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. */ 500: { headers: { [name: string]: unknown; @@ -2620,7 +3896,7 @@ export interface operations { }; }; }; - getJobs: { + getCoverageSelect: { parameters: { query?: never; header?: never; @@ -2635,7 +3911,7 @@ export interface operations { [name: string]: unknown; }; content: { - '*/*': components['schemas']['BaseResponseJobResponses']; + '*/*': components['schemas']['BaseResponseCoveragePreferenceResponses']; }; }; /** @description ๊ฒฝ๋กœ ๋ณ€์ˆ˜ ๊ฐ’์ด ๋ˆ„๋ฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. */ @@ -2694,9 +3970,12 @@ export interface operations { }; }; }; - getDiagnosedDisease: { + getTrendPosts: { parameters: { - query?: never; + query?: { + size?: number; + sort?: string; + }; header?: never; path?: never; cookie?: never; @@ -2709,7 +3988,7 @@ export interface operations { [name: string]: unknown; }; content: { - '*/*': components['schemas']['BaseResponseDiagnosedDiseaseResponses']; + '*/*': components['schemas']['BaseResponseTrendingPostsResponses']; }; }; /** @description ๊ฒฝ๋กœ ๋ณ€์ˆ˜ ๊ฐ’์ด ๋ˆ„๋ฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. */ @@ -2768,9 +4047,13 @@ export interface operations { }; }; }; - getCoverageSelect: { + searchPosts: { parameters: { - query?: never; + query: { + keyword: string; + cursor?: string; + size?: number; + }; header?: never; path?: never; cookie?: never; @@ -2783,10 +4066,9 @@ export interface operations { [name: string]: unknown; }; content: { - '*/*': components['schemas']['BaseResponseCoveragePreferenceResponses']; + '*/*': components['schemas']['BaseResponseSliceResponsePostSearchResponseString']; }; }; - /** @description ๊ฒฝ๋กœ ๋ณ€์ˆ˜ ๊ฐ’์ด ๋ˆ„๋ฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. */ 400: { headers: { [name: string]: unknown; @@ -2795,7 +4077,6 @@ export interface operations { 'application/json': unknown; }; }; - /** @description ์œ ํšจํ•˜์ง€ ์•Š์€ JWT์ž…๋‹ˆ๋‹ค. */ 401: { headers: { [name: string]: unknown; @@ -2804,7 +4085,6 @@ export interface operations { 'application/json': unknown; }; }; - /** @description ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค. */ 403: { headers: { [name: string]: unknown; @@ -2813,7 +4093,6 @@ export interface operations { 'application/json': unknown; }; }; - /** @description ์ง€์›ํ•˜์ง€ ์•Š๋Š” URL์ž…๋‹ˆ๋‹ค. */ 404: { headers: { [name: string]: unknown; @@ -2822,7 +4101,6 @@ export interface operations { 'application/json': unknown; }; }; - /** @description ์œ ํšจํ•˜์ง€ ์•Š์€ Http ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค. */ 405: { headers: { [name: string]: unknown; @@ -2831,7 +4109,6 @@ export interface operations { 'application/json': unknown; }; }; - /** @description ์™ธ๋ถ€ ์„œ๋ฒ„ ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. */ 500: { headers: { [name: string]: unknown; @@ -3272,14 +4549,11 @@ export interface operations { }; }; }; - deleteComment: { + unlink: { parameters: { query?: never; header?: never; - path: { - 'post-id': number; - 'comment-id': number; - }; + path?: never; cookie?: never; }; requestBody?: never; @@ -3293,6 +4567,7 @@ export interface operations { '*/*': components['schemas']['BaseResponseVoid']; }; }; + /** @description ๊ฒฝ๋กœ ๋ณ€์ˆ˜ ๊ฐ’์ด ๋ˆ„๋ฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. */ 400: { headers: { [name: string]: unknown; @@ -3301,6 +4576,7 @@ export interface operations { 'application/json': unknown; }; }; + /** @description ์œ ํšจํ•˜์ง€ ์•Š์€ JWT์ž…๋‹ˆ๋‹ค. */ 401: { headers: { [name: string]: unknown; @@ -3309,6 +4585,7 @@ export interface operations { 'application/json': unknown; }; }; + /** @description ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค. */ 403: { headers: { [name: string]: unknown; @@ -3317,6 +4594,7 @@ export interface operations { 'application/json': unknown; }; }; + /** @description ์ง€์›ํ•˜์ง€ ์•Š๋Š” URL์ž…๋‹ˆ๋‹ค. */ 404: { headers: { [name: string]: unknown; @@ -3325,6 +4603,7 @@ export interface operations { 'application/json': unknown; }; }; + /** @description ์œ ํšจํ•˜์ง€ ์•Š์€ Http ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค. */ 405: { headers: { [name: string]: unknown; @@ -3333,14 +4612,7 @@ export interface operations { 'application/json': unknown; }; }; - 409: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': unknown; - }; - }; + /** @description ์™ธ๋ถ€ ์„œ๋ฒ„ ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. */ 500: { headers: { [name: string]: unknown; diff --git a/apps/client/src/shared/types/type.ts b/apps/client/src/shared/types/type.ts index 0b4ab7886..b49a53b53 100644 --- a/apps/client/src/shared/types/type.ts +++ b/apps/client/src/shared/types/type.ts @@ -1 +1,13 @@ export type StatusType = '์ถฉ๋ถ„' | '๋ถ€์กฑ' | '๊ฐ•๋ ฅ'; +export type ChipColorType = 'primary600' | 'bofitOrange' | 'error' | 'gray800'; + +export interface Image { + imageId?: number; + imageUrl?: string; +} + +export const STATUS_COLOR_MAP: Record = { + ์ถฉ๋ถ„: 'primary600', + ๋ถ€์กฑ: 'bofitOrange', + ๊ฐ•๋ ฅ: 'error', +}; diff --git a/apps/client/src/shared/utils/format-price.ts b/apps/client/src/shared/utils/format-price.ts deleted file mode 100644 index 841dca93e..000000000 --- a/apps/client/src/shared/utils/format-price.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const formatPrice = (price: number): string => { - return price.toLocaleString('ko-KR'); -}; diff --git a/apps/client/src/shared/utils/get-time-ago.ts b/apps/client/src/shared/utils/get-time-ago.ts deleted file mode 100644 index 50962c413..000000000 --- a/apps/client/src/shared/utils/get-time-ago.ts +++ /dev/null @@ -1,33 +0,0 @@ -export const getTimeAgo = (createdAt?: string): string => { - if (!createdAt) { - return ''; - } - - const now = new Date(); - const created = new Date(createdAt); - - const createdTime = created.getTime(); - if (isNaN(createdTime)) { - return ''; - } - - const diffMs = now.getTime() - created.getTime(); - - const diffSec = Math.floor(diffMs / 1000); - const diffMin = Math.floor(diffSec / 60); - const diffHour = Math.floor(diffMin / 60); - - if (diffHour < 1) { - return `${Math.max(1, diffMin)}๋ถ„ ์ „`; - } - - if (diffHour < 24) { - return `${diffHour}์‹œ๊ฐ„ ์ „`; - } - - const year = created.getFullYear(); - const month = String(created.getMonth() + 1).padStart(2, '0'); - const day = String(created.getDate()).padStart(2, '0'); - - return `${year}-${month}-${day}`; -}; diff --git a/apps/client/src/shared/utils/utils.ts b/apps/client/src/shared/utils/utils.ts new file mode 100644 index 000000000..846c75398 --- /dev/null +++ b/apps/client/src/shared/utils/utils.ts @@ -0,0 +1,73 @@ +/** + * presigned URL ๋ฐฐ์—ด์—์„œ ์‹ค์ œ S3 URL๋งŒ ์ถ”์ถœ + * + * @param {string[]} presignedUrls - presigned URL ๋ฐฐ์—ด + * @returns {string[]} ์ฟผ๋ฆฌ ์ŠคํŠธ๋ง์„ ์ œ๊ฑฐํ•œ ์‹ค์ œ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ S3 URL ๋ฐฐ์—ด + * + * @example + * const urls = [ + * "https://bucket.s3.amazonaws.com/image.jpg?X-Amz-Signature=xxx", + * "https://bucket.s3.amazonaws.com/image2.jpg?X-Amz-Signature=yyy" + * ]; + * extractS3Urls(urls); + * // ["https://bucket.s3.amazonaws.com/image.jpg", "https://bucket.s3.amazonaws.com/image2.jpg"] + */ +export const extractS3Urls = (presignedUrls: string[]): string[] => { + return presignedUrls.map((url) => url.split('?')[0]); +}; + +/** + * ํŠน์ • ์‹œ๊ฐ„์œผ๋กœ๋ถ€ํ„ฐ ์–ผ๋งˆ๋‚˜ ์ง€๋‚ฌ๋Š”์ง€ "๋ช‡ ๋ถ„ ์ „ / ๋ช‡ ์‹œ๊ฐ„ ์ „ / YYYY-MM-DD" ํ˜•์‹์œผ๋กœ ๋ฐ˜ํ™˜ + * + * @param {string} [createdAt] - ๊ธฐ์ค€ ์‹œ๊ฐ„ (ISO ๋ฌธ์ž์—ด) + * @returns {string} ๊ฒฝ๊ณผ ์‹œ๊ฐ„ ๋ฌธ์ž์—ด, `createdAt`์ด ์—†๊ฑฐ๋‚˜ ์œ ํšจํ•˜์ง€ ์•Š์œผ๋ฉด ๋นˆ ๋ฌธ์ž์—ด ๋ฐ˜ํ™˜ + * + * @example + * getTimeAgo("2025-10-06T05:00:00Z"); // "30๋ถ„ ์ „" ํ˜น์€ "2025-10-06" + */ +export const getTimeAgo = (createdAt?: string): string => { + if (!createdAt) { + return ''; + } + + const now = new Date(); + const created = new Date(createdAt); + + const createdTime = created.getTime(); + if (isNaN(createdTime)) { + return ''; + } + + const diffMs = now.getTime() - created.getTime(); + + const diffSec = Math.floor(diffMs / 1000); + const diffMin = Math.floor(diffSec / 60); + const diffHour = Math.floor(diffMin / 60); + + if (diffHour < 1) { + return `${Math.max(1, diffMin)}๋ถ„ ์ „`; + } + + if (diffHour < 24) { + return `${diffHour}์‹œ๊ฐ„ ์ „`; + } + + const year = created.getFullYear(); + const month = String(created.getMonth() + 1).padStart(2, '0'); + const day = String(created.getDate()).padStart(2, '0'); + + return `${year}-${month}-${day}`; +}; + +/** + * ์ˆซ์ž ๊ฐ€๊ฒฉ์„ ํ•œ๊ตญ ์›ํ™” ํ‘œ๊ธฐ๋ฒ•์œผ๋กœ ํฌ๋งทํŒ… + * + * @param {number} price - ํฌ๋งทํŒ…ํ•  ๊ฐ€๊ฒฉ + * @returns {string} ์ฝค๋งˆ๊ฐ€ ํฌํ•จ๋œ ๊ฐ€๊ฒฉ ๋ฌธ์ž์—ด + * + * @example + * formatPrice(1234567); // "1,234,567" + */ +export const formatPrice = (price: number): string => { + return price.toLocaleString('ko-KR'); +}; diff --git a/apps/client/src/widgets/community/components/.gitkeep b/apps/client/src/widgets/community/components/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/client/src/widgets/community/components/comment-input-box/comment-input-box.css.ts b/apps/client/src/widgets/community/components/comment-input-box/comment-input-box.css.ts index 8c0b437f3..5db099758 100644 --- a/apps/client/src/widgets/community/components/comment-input-box/comment-input-box.css.ts +++ b/apps/client/src/widgets/community/components/comment-input-box/comment-input-box.css.ts @@ -2,14 +2,56 @@ import { style } from '@vanilla-extract/css'; import { themeVars } from '@bds/ui/styles'; -export const container = style({ +export const commentWrapper = style({ display: 'flex', + flexDirection: 'column', position: 'fixed', bottom: '0', - padding: '1.4rem 1.6rem', + width: '100%', maxWidth: '43rem', - gap: '0.8rem', - backgroundColor: themeVars.color.whiteBackground, borderTop: `1px solid ${themeVars.color.gray100}`, + + backgroundColor: themeVars.color.whiteBackground, + zIndex: themeVars.zIndex.overlay, +}); + +export const imagePreviewWrapper = style({ + display: 'flex', + flexDirection: 'row', + padding: '1rem 1.6rem', + justifyContent: 'space-between', +}); + +export const previewImage = style({ + height: '4.7rem', + objectFit: 'cover', +}); + +export const inputWrapper = style({ + display: 'flex', + padding: '1.2rem 1.6rem', +}); + +export const controlWrapper = style({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', +}); + +export const imageWrapper = style({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + gap: '1.6rem', + padding: '0.8rem 0 1.4rem 1.6rem', + color: themeVars.color.gray800, + ...themeVars.fontStyles.title_sb_16, +}); + +export const buttonWrapper = style({ + display: 'flex', + flexDirection: 'row', + gap: '1rem', + padding: '0 1.6rem 0.6rem 0', }); diff --git a/apps/client/src/widgets/community/components/comment-input-box/comment-input-box.tsx b/apps/client/src/widgets/community/components/comment-input-box/comment-input-box.tsx index 0c4b253d9..2d150dea2 100644 --- a/apps/client/src/widgets/community/components/comment-input-box/comment-input-box.tsx +++ b/apps/client/src/widgets/community/components/comment-input-box/comment-input-box.tsx @@ -1,9 +1,12 @@ -import { ChangeEvent, KeyboardEvent } from 'react'; +import { ChangeEvent, KeyboardEvent, useRef } from 'react'; -import { Input } from '@bds/ui'; +import { Input, useModal } from '@bds/ui'; import { Icon } from '@bds/ui/icons'; import { PLACEHOLDER } from '@widgets/community/constant/input-placeholder'; +import { useChangeInputMode } from '@widgets/community/context/input-mode-context'; + +import CommunityModal from '../community-modal/community-modal'; import * as styles from './comment-input-box.css'; @@ -11,7 +14,12 @@ interface CommentInputBoxProps { value: string; onChange: (e: ChangeEvent) => void; errorState?: boolean; - onSubmit: () => void; + onSubmit: (file?: File) => void; + focusKey?: string; + selectedFile: File | null; + previewUrl?: string; + onImageChange: (file: File | null) => void; + onClearImage?: () => void; } const CommentInputBox = ({ @@ -19,36 +27,161 @@ const CommentInputBox = ({ onChange, errorState, onSubmit, + focusKey, + selectedFile, + previewUrl, + onImageChange, + onClearImage, }: CommentInputBoxProps) => { + const fileInputRef = useRef(null); + const { mode, dispatch } = useChangeInputMode(); + const { openModal, closeModal } = useModal(); + + const modalType: 'create' | 'edit' = + (mode.type === 'comment' || mode.type === 'reply') && mode.action === 'edit' + ? 'edit' + : 'create'; + const handleKeyDown = (e: KeyboardEvent) => { if (e.nativeEvent.isComposing) { return; } - if (e.key === 'Enter') { e.preventDefault(); - onSubmit(); + handleSubmit(); + } + }; + + const handleFileChange = (e: ChangeEvent) => { + onImageChange(e.target.files?.[0] ?? null); + }; + + const handleOpenFileDialog = () => { + (document.activeElement as HTMLElement)?.blur(); + setTimeout(() => fileInputRef.current?.click(), 100); + }; + + const handleRemoveAllClick = () => { + openModal( + { + onChange({ target: { value: '' } } as ChangeEvent); + onClearImage?.(); + dispatch({ type: 'RESET' }); + closeModal(); + }} + />, + ); + }; + + const handleRemoveImage = () => { + onImageChange(null); + onClearImage?.(); + if (fileInputRef.current) { + fileInputRef.current.value = ''; } }; + const handleSubmit = () => { + const trimmed = value.trim(); + if (!trimmed && !selectedFile) { + return; + } + + onSubmit(selectedFile || undefined); + onChange({ target: { value: '' } } as ChangeEvent); + handleRemoveImage(); + + if (modalType === 'edit') { + dispatch({ type: 'RESET' }); + } + }; + + const shouldShowClear = !!value.trim() || selectedFile || !!previewUrl; + const displayImage = selectedFile + ? URL.createObjectURL(selectedFile) + : previewUrl; + return ( -
- - -
+
+ {displayImage && ( +
+ preview selectedFile && URL.revokeObjectURL(displayImage)} + /> + +
+ )} + +
+ +
+ +
+ + + + +

์‚ฌ์ง„ ์˜ฌ๋ฆฌ๊ธฐ

+
+ + + {shouldShowClear && ( + + )} + + +
+
); }; diff --git a/apps/client/src/widgets/community/components/community-image-uploader/community-image-uploader.css.ts b/apps/client/src/widgets/community/components/community-image-uploader/community-image-uploader.css.ts new file mode 100644 index 000000000..711c3ec04 --- /dev/null +++ b/apps/client/src/widgets/community/components/community-image-uploader/community-image-uploader.css.ts @@ -0,0 +1,28 @@ +import { style } from '@vanilla-extract/css'; + +import { themeVars } from '@bds/ui/styles'; + +export const imageUploaderContainer = style({ + position: 'absolute', + bottom: 0, + display: 'flex', + width: '100%', + height: '4.6rem', + padding: '0.8rem 1.6rem', + alignItems: 'center', + backgroundColor: themeVars.color.white, + cursor: 'pointer', +}); + +export const imageUploadText = style({ + ...themeVars.fontStyles.title_sb_16, + color: themeVars.color.gray800, + display: 'flex', + padding: '0.6rem 1.6rem', + justifyContent: 'center', + alignItems: 'center', +}); + +export const imageHiddenInput = style({ + display: 'none', +}); diff --git a/apps/client/src/widgets/community/components/community-image-uploader/community-image-uploader.tsx b/apps/client/src/widgets/community/components/community-image-uploader/community-image-uploader.tsx new file mode 100644 index 000000000..5589afcfc --- /dev/null +++ b/apps/client/src/widgets/community/components/community-image-uploader/community-image-uploader.tsx @@ -0,0 +1,25 @@ +import { Icon } from '@bds/ui/icons'; + +import * as styles from './community-image-uploader.css'; + +interface CommunityImageUploaderProps { + onChange: (files: FileList) => void; +} + +const CommunityImageUploader = ({ onChange }: CommunityImageUploaderProps) => { + return ( + + ); +}; + +export default CommunityImageUploader; diff --git a/apps/client/src/widgets/community/components/community-line/community-line.css.ts b/apps/client/src/widgets/community/components/community-line/community-line.css.ts index 958a585de..6b5f02211 100644 --- a/apps/client/src/widgets/community/components/community-line/community-line.css.ts +++ b/apps/client/src/widgets/community/components/community-line/community-line.css.ts @@ -7,17 +7,15 @@ export const postBody = style({ flexDirection: 'column', padding: '1.2rem 0 2.6rem', width: '100%', - height: '100%', borderTop: `1px solid ${themeVars.color.gray100}`, }); export const inputContent = style({ - width: '100%', - minHeight: '34.3rem', - height: 'calc(100svh - 276px)', ...themeVars.fontStyles.body2_r_14, + width: '100%', color: themeVars.color.gray600, outline: 'none', + resize: 'none', background: 'transparent', border: 'none', selectors: { diff --git a/apps/client/src/widgets/community/components/community-line/community-line.tsx b/apps/client/src/widgets/community/components/community-line/community-line.tsx index 973096492..570756ac5 100644 --- a/apps/client/src/widgets/community/components/community-line/community-line.tsx +++ b/apps/client/src/widgets/community/components/community-line/community-line.tsx @@ -1,4 +1,5 @@ import { useRef } from 'react'; +import TextareaAutosize from 'react-textarea-autosize'; import { COMMUNITY_LINE_PLACEHOLDER } from '@widgets/community/constant/community-line-placeholder'; @@ -22,8 +23,8 @@ const CommunityLine = ({ value, onChange, onSubmit }: CommunityLineProps) => { return (
-