diff --git a/package.json b/package.json index fa4ca587..2cc4b1f9 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "re:gen:api": "node ./scripts/re-create-api-json.js" }, "dependencies": { - "@amplitude/analytics-browser": "^2.11.6", + "@amplitude/analytics-browser": "^2.17.6", "@hookform/resolvers": "^3.3.4", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-dialog": "^1.1.1", @@ -24,16 +24,17 @@ "@radix-ui/react-menubar": "^1.1.1", "@radix-ui/react-popover": "^1.1.1", "@radix-ui/react-slot": "^1.1.1", - "@tanstack/react-query": "^5.64.2", - "@tanstack/react-query-devtools": "^5.51.23", + "@tanstack/react-query": "^5.79.2", + "@tanstack/react-query-devtools": "^5.79.2", "@vanilla-extract/css": "^1.17.0", "@vanilla-extract/recipes": "^0.5.5", - "axios": "^1.6.8", - "country-flag-icons": "^1.5.13", + "axios": "^1.9.0", + "clsx": "^2.1.1", + "country-flag-icons": "^1.5.19", "date-fns": "^3.6.0", "embla-carousel-autoplay": "^8.2.0", "embla-carousel-react": "^8.1.8", - "es-toolkit": "^1.27.0", + "es-toolkit": "^1.38.0", "framer-motion": "^11.3.24", "heic2any": "^0.0.4", "html2canvas": "^1.4.1", @@ -41,32 +42,32 @@ "jotai": "^2.8.0", "lucide-react": "^0.441.0", "qs": "^6.13.0", - "react": "^18.2.0", + "react": "^18.3.1", "react-day-picker": "^8.10.1", - "react-dom": "^18.2.0", + "react-dom": "^18.3.1", "react-error-boundary": "^5.0.0", - "react-helmet-async": "^2.0.4", + "react-helmet-async": "^2.0.5", "react-hook-form": "^7.51.4", "react-icons": "^5.5.0", "react-image-file-resizer": "^0.4.8", - "react-router-dom": "^6.22.3", + "react-router-dom": "^6.30.1", "react-select": "^5.8.0", "react-textarea-autosize": "^8.5.3", - "sonner": "^1.5.0", - "ts-pattern": "^5.3.1", - "zod": "^3.23.8" + "sonner": "^1.7.4", + "ts-pattern": "^5.7.1", + "zod": "^3.25.49" }, "devDependencies": { "@openapitools/openapi-generator-cli": "^2.16.3", "@pandacss/dev": "^0.37.2", "@tanstack/eslint-plugin-query": "^5.51.15", - "@types/qs": "^6.9.16", - "@types/react": "^18.2.66", - "@types/react-dom": "^18.2.22", + "@types/qs": "^6.14.0", + "@types/react": "^18.3.23", + "@types/react-dom": "^18.3.7", "@typescript-eslint/eslint-plugin": "^7.2.0", "@typescript-eslint/parser": "^7.2.0", "@vanilla-extract/vite-plugin": "^4.0.19", - "@vitejs/plugin-react-swc": "^3.5.0", + "@vitejs/plugin-react-swc": "^3.10.0", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-import-resolver-typescript": "^3.6.1", @@ -75,8 +76,8 @@ "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.6", "prettier": "^3.2.5", - "typescript": "^5.2.2", - "vite": "^5.2.0", + "typescript": "^5.8.3", + "vite": "^5.4.19", "vite-tsconfig-paths": "^4.3.2" }, "packageManager": "yarn@4.6.0" diff --git a/packages/api/ku-key/.openapi-generator/FILES b/packages/api/ku-key/.openapi-generator/FILES index bc87100f..cff524de 100644 --- a/packages/api/ku-key/.openapi-generator/FILES +++ b/packages/api/ku-key/.openapi-generator/FILES @@ -76,6 +76,7 @@ models/get-friend-response-dto.ts models/get-hot-club-response-dto.ts models/get-my-comment-list-response-dto.ts models/get-notice-response-dto.ts +models/get-nullable-timetable-response-dto.ts models/get-point-history-response-dto.ts models/get-post-list-response-dto.ts models/get-post-list-with-board-response-dto.ts diff --git a/packages/api/ku-key/api/banner-api.ts b/packages/api/ku-key/api/banner-api.ts index d3dcf7b0..32714852 100644 --- a/packages/api/ku-key/api/banner-api.ts +++ b/packages/api/ku-key/api/banner-api.ts @@ -95,12 +95,14 @@ const bannerIdDeleteAxiosParamCreator = async ( * @summary 배너 이미지 생성 * @param {any} image 배너 이미지 파일 * @param {string} title 배너 제목 + * @param {string} [link] 링크 * @param {*} [options] Override http request option. * @throws {RequiredError} */ const bannerPostAxiosParamCreator = async ( image: any, title: string, + link?: string, options: AxiosRequestConfig = {}, configuration?: Configuration, ): Promise => { @@ -125,6 +127,10 @@ const bannerPostAxiosParamCreator = async ( localVarFormParams.append('title', title as any) } + if (link !== undefined) { + localVarFormParams.append('link', link as any) + } + localVarHeaderParameter['Content-Type'] = 'multipart/form-data' setSearchParams(localVarUrlObj, localVarQueryParameter) @@ -175,16 +181,18 @@ const bannerIdDeleteFp = async ( * @summary 배너 이미지 생성 * @param {any} image 배너 이미지 파일 * @param {string} title 배너 제목 + * @param {string} [link] 링크 * @param {*} [options] Override http request option. * @throws {RequiredError} */ const bannerPostFp = async ( image: any, title: string, + link?: string, options?: AxiosRequestConfig, configuration?: Configuration, ): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> => { - const localVarAxiosArgs = await bannerPostAxiosParamCreator(image, title, options, configuration) + const localVarAxiosArgs = await bannerPostAxiosParamCreator(image, title, link, options, configuration) return createRequestFunction(localVarAxiosArgs, globalAxios, configuration) } @@ -246,6 +254,7 @@ export const bannerIdDelete = ({ export type BannerPostRequestParams = { image: any title: string + link?: string options?: any } @@ -254,6 +263,7 @@ export type BannerPostRequestParams = { * @summary 배너 이미지 생성 * @param {any} image 배너 이미지 파일 * @param {string} title 배너 제목 + * @param {string} [link] 링크 * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -267,7 +277,7 @@ export const bannerPost = ({ axios?: AxiosInstance }) => { return (params: BannerPostRequestParams): AxiosPromise => { - return bannerPostFp(params.image, params.title, params.options, configuration).then(request => + return bannerPostFp(params.image, params.title, params.link, params.options, configuration).then(request => request(axios, basePath), ) } diff --git a/packages/api/ku-key/api/friendship-api.ts b/packages/api/ku-key/api/friendship-api.ts index 886a9757..6212400d 100644 --- a/packages/api/ku-key/api/friendship-api.ts +++ b/packages/api/ku-key/api/friendship-api.ts @@ -25,9 +25,9 @@ import { DeleteFriendshipResponseDto } from '../models' // @ts-ignore import { GetFriendResponseDto } from '../models' // @ts-ignore -import { GetReceivedFriendshipRequestCountDto } from '../models' +import { GetNullableTimetableResponseDto } from '../models' // @ts-ignore -import { GetTimetableByTimetableIdDto } from '../models' +import { GetReceivedFriendshipRequestCountDto } from '../models' // @ts-ignore import { GetWaitingFriendResponseDto } from '../models' // @ts-ignore @@ -455,7 +455,7 @@ const friendshipFriendTimetableGetFp = async ( semester: string, options?: AxiosRequestConfig, configuration?: Configuration, -): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> => { +): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> => { const localVarAxiosArgs = await friendshipFriendTimetableGetAxiosParamCreator( username, year, @@ -652,7 +652,7 @@ export const friendshipFriendTimetableGet = ({ basePath?: string axios?: AxiosInstance }) => { - return (params: FriendshipFriendTimetableGetRequestParams): AxiosPromise> => { + return (params: FriendshipFriendTimetableGetRequestParams): AxiosPromise => { return friendshipFriendTimetableGetFp( params.username, params.year, diff --git a/packages/api/ku-key/api/timetable-api.ts b/packages/api/ku-key/api/timetable-api.ts index e86827f2..b7c6855b 100644 --- a/packages/api/ku-key/api/timetable-api.ts +++ b/packages/api/ku-key/api/timetable-api.ts @@ -29,7 +29,7 @@ import { CreateTimetableCourseResponseDto } from '../models' // @ts-ignore import { CreateTimetableDto } from '../models' // @ts-ignore -import { GetTimetableByTimetableIdDto } from '../models' +import { GetNullableTimetableResponseDto } from '../models' // @ts-ignore import { GetTimetableByUserIdResponseDto } from '../models' // @ts-ignore @@ -606,7 +606,7 @@ const timetableTimetableIdGetFp = async ( timetableId: number, options?: AxiosRequestConfig, configuration?: Configuration, -): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> => { +): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> => { const localVarAxiosArgs = await timetableTimetableIdGetAxiosParamCreator(timetableId, options, configuration) return createRequestFunction(localVarAxiosArgs, globalAxios, configuration) } @@ -901,7 +901,7 @@ export const timetableTimetableIdGet = ({ basePath?: string axios?: AxiosInstance }) => { - return (params: TimetableTimetableIdGetRequestParams): AxiosPromise => { + return (params: TimetableTimetableIdGetRequestParams): AxiosPromise => { return timetableTimetableIdGetFp(params.timetableId, params.options, configuration).then(request => request(axios, basePath), ) diff --git a/packages/api/ku-key/models/banner-dto.ts b/packages/api/ku-key/models/banner-dto.ts index 57ddae14..a81f5071 100644 --- a/packages/api/ku-key/models/banner-dto.ts +++ b/packages/api/ku-key/models/banner-dto.ts @@ -36,4 +36,10 @@ export interface BannerDto { * @memberof BannerDto */ title: string + /** + * 배너 링크 + * @type {string} + * @memberof BannerDto + */ + link: string | null } diff --git a/packages/api/ku-key/models/get-nullable-timetable-response-dto.ts b/packages/api/ku-key/models/get-nullable-timetable-response-dto.ts new file mode 100644 index 00000000..7a30ffa0 --- /dev/null +++ b/packages/api/ku-key/models/get-nullable-timetable-response-dto.ts @@ -0,0 +1,29 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * KU-KEY API + * API for KU-KEY service + * + * The version of the OpenAPI document: 1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { GetTimetableByTimetableIdDto } from './get-timetable-by-timetable-id-dto' + +/** + * + * @export + * @interface GetNullableTimetableResponseDto + */ +export interface GetNullableTimetableResponseDto { + /** + * 특정 시간표 정보 + * @type {GetTimetableByTimetableIdDto} + * @memberof GetNullableTimetableResponseDto + */ + timetable: GetTimetableByTimetableIdDto | null +} diff --git a/packages/api/ku-key/models/index.ts b/packages/api/ku-key/models/index.ts index 25b2156b..ee30fbf6 100644 --- a/packages/api/ku-key/models/index.ts +++ b/packages/api/ku-key/models/index.ts @@ -50,6 +50,7 @@ export * from './get-friend-response-dto' export * from './get-hot-club-response-dto' export * from './get-my-comment-list-response-dto' export * from './get-notice-response-dto' +export * from './get-nullable-timetable-response-dto' export * from './get-point-history-response-dto' export * from './get-post-list-response-dto' export * from './get-post-list-with-board-response-dto' diff --git a/scripts/re-create-api-json.js b/scripts/re-create-api-json.js index 8e2aca0c..dc1df067 100644 --- a/scripts/re-create-api-json.js +++ b/scripts/re-create-api-json.js @@ -37,5 +37,5 @@ async function removeOperationId(filePath) { } // 스크립트 실행 -await removeOperationId('https://15.164.27.130.nip.io/api-json') +await removeOperationId('https://dev.api.ku-key.devkor.club/api-json') await execShellCommand(`yarn prettier --w "scripts/__generated__/ku-key-api.json"`) diff --git a/src/components/ui/profile/index.tsx b/src/components/ui/profile/index.tsx index 90cc56f4..ddcde725 100644 --- a/src/components/ui/profile/index.tsx +++ b/src/components/ui/profile/index.tsx @@ -16,16 +16,19 @@ const bgConfig: Record = { interface ProfileProps extends Pick { onlyTitle: boolean bgWhite?: boolean + size?: number } -const Profile = ({ isAnonymous, isDeleted, character, onlyTitle, bgWhite }: ProfileProps) => { +const Profile = ({ isAnonymous, isDeleted, character, onlyTitle, bgWhite, size = 20 }: ProfileProps) => { const profileImg = characterConfig[character.type][character.level ?? 1] const bgColor = bgConfig[character.type] + + const w = size ?? (onlyTitle ? 15 : 20) return ( Profile ) diff --git a/src/domain/Post/components/MobileFeedItem/index.tsx b/src/domain/Post/components/MobileFeedItem/index.tsx new file mode 100644 index 00000000..19a4519d --- /dev/null +++ b/src/domain/Post/components/MobileFeedItem/index.tsx @@ -0,0 +1,152 @@ +import { useCallback, useMemo } from 'react' +import { LuBookmark, LuBookText, LuCookie, LuMessageCircle, LuMessageCircleQuestion, LuUsers } from 'react-icons/lu' +import { useNavigate } from 'react-router-dom' +import { match } from 'ts-pattern' + +import * as s from './style.css' + +import Profile from '@/components/ui/profile' +import { PostPreviewWithBoardName } from '@/packages/api/ku-key/models' +import { vars } from '@/theme/theme.css' +import { Typography } from '@/ui/Typography' + +type Props = Pick< + PostPreviewWithBoardName, + | 'id' + | 'title' + | 'content' + | 'user' + | 'reactionCount' + | 'commentCount' + | 'scrapCount' + | 'commentCount' + | 'scrapCount' + | 'myScrap' + | 'thumbnailDir' + | 'boardName' +> & { + showCommunityBadge?: boolean +} +const MobileFeedItem = ({ + id, + title, + content, + user, + reactionCount, + commentCount, + scrapCount, + myScrap, + thumbnailDir, + boardName, + showCommunityBadge, +}: Props) => { + const navigate = useNavigate() + const handleNavigate = useCallback( + () => navigate(`/community/${boardName.split(' ')[0].toLowerCase()}/post/${id}`), + [navigate, boardName, id], + ) + const svg = useMemo( + () => + match(boardName) + .with('Community Board', () => ) + .with('Question Board', () => ) + .with('Information Board', () => ) + .otherwise(() => ), + [boardName], + ) + return ( +
+
+ {thumbnailDir && ( +
+ )} +
+ {showCommunityBadge && ( +
+ {svg} + + {boardName} + +
+ )} +
+ + {title} + +
+
+ + {content} + +
+
+
+
+
+ + + {user.isAnonymous ? 'Anonymous' : user.username} + +
+
+
+ + + {reactionCount} + +
+
+ + + {commentCount} + +
+
+ + + {scrapCount} + +
+
+
+
+ ) +} + +export default MobileFeedItem diff --git a/src/domain/Post/components/MobileFeedItem/style.css.ts b/src/domain/Post/components/MobileFeedItem/style.css.ts new file mode 100644 index 00000000..26fa40aa --- /dev/null +++ b/src/domain/Post/components/MobileFeedItem/style.css.ts @@ -0,0 +1,58 @@ +import { style } from '@vanilla-extract/css' + +import { f } from '@/style' +import { vars } from '@/theme/theme.css' + +export const Wrapper = style([ + f.flex, + f.wFull, + f.directionColumn, + f.alignStart, + { + gap: '0.625rem', + paddingBottom: '1rem', + borderBottom: `1px solid ${vars.color.lightGray2}`, + selectors: { + '&:last-child': { + borderBottom: 'none', + }, + }, + }, +]) + +export const BodyWrapper = style([f.flex, f.wFull, f.alignCenter, { gap: '0.875rem' }]) + +export const Image = style([ + f.flex, + { + width: '5.625rem', + aspectRatio: '1/1', + objectFit: 'cover', + flexShrink: 0, + borderRadius: '11px', + border: `1px solid ${vars.color.lightGray1}`, + }, +]) + +export const Text = style([ + f.flex, + f.directionColumn, + f.justifyBetween, + f.alignStart, + f.wFull, + { height: '5.625rem', padding: '0.125rem 0' }, +]) + +export const Tag = style([f.flex, f.alignCenter, { gap: '0.25rem' }]) + +export const Icon = style([f.color.static.red3, { width: '0.8125rem', height: '0.8125rem' }]) + +export const TypoWrapper = style([f.wFull, f.flex, f.alignCenter]) + +export const FeedbackWrapper = style([f.flex, f.wFull, f.justifyBetween, f.alignCenter]) + +export const Profile = style([f.flex, f.alignCenter, { gap: '0.375rem' }]) + +export const FeedBack = style([f.flex, f.alignCenter, f.color.static.darkGray2, { gap: '0.625rem' }]) + +export const FeedBackIcon = style([f.flex, f.alignCenter, { gap: '0.25rem' }]) diff --git a/src/features/Community/CommunitySelectTab/index.tsx b/src/features/Community/CommunitySelectTab/index.tsx index 9f9a4a06..85497489 100644 --- a/src/features/Community/CommunitySelectTab/index.tsx +++ b/src/features/Community/CommunitySelectTab/index.tsx @@ -1,6 +1,7 @@ import * as s from './style.css' import { Button } from '@/ui/Button' +import { useMediaQueryByName } from '@/util/hooks/useMediaQueryByName' import { useQueryParams } from '@/util/hooks/useQueryParams' const boardConfig = [ @@ -34,12 +35,14 @@ export type BoardQueryParam = { const CommunitySelectTab = () => { const [queryParam, setQueryParam] = useQueryParams() - + const isMobile = useMediaQueryByName('smDown') return (
{boardConfig.map(board => (