diff --git a/README.md b/README.md
index 488fbc69..d725604f 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,89 @@
-# InvestMetic
-
-
+
+๐ [investmetic](https://www.investmetic.co.kr/)
+
+**investmetic**์ **ํฌ์ ๋งค๋งค ์ ๋ต ๊ณต์ ๋ฐ
+์ค๊ฐ์์
ํ๋ซํผ ์๋น์ค** ์
๋๋ค.
+
+## ํ
์คํธ ๊ณ์
+
+| **์ญํ ** | **์ด๋ฉ์ผ** | **๋น๋ฐ๋ฒํธ** |
+| ------------ | -------------------- | ------------ |
+| **ํฌ์์** | investor@example.com | investor123 |
+| **ํธ๋ ์ด๋** | trader@example.com | trader123 |
+
+
+
+## ์ญํ ๋ถ๋ด
+
+
+
+| [

](https://github.com/devdeun) | [

](https://github.com/nanafromjeju) | [

](https://github.com/ssumanlife) | [

](https://github.com/kimpra2989) | [

](https://github.com/HSjjs98) |
+| :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
+| [๐ @Deun](https://github.com/devdeun) | [@Nana](https://github.com/nanafromjeju) | [@SuMin](https://github.com/ssumanlife) | [@Kimpra](https://github.com/kimpra2989) | [@James](https://github.com/HSjjs98) |
+| ํ๋ก์ ํธ ์ด๊ธฐ ์ธํ
(NextJS, ๋ฆฐํธ ๋ฐ Prettier,
๊ธฐ๋ณธ ์คํ์ผ ๋ฐ SCSS, lefthook)
๊ณตํต ์ปดํฌ๋ํธ
(์ฌ์ด๋๋ฐ, ํญ๋ฉ๋ด, ์๋ฐํ,
ํธํฐ, ๋ก๋ฉ์คํผ๋, ๋๋ฉ ๋ฉ์ธ ์ฐจํธ)
๋๋ฉ ํ์ด์ง,
ํ์๊ฐ์
ํ์ด์ง,
๊ตฌ๋
ํ ์ ๋ต ํ์ด์ง,
๋ฌธ์๋ด์ญ ํ์ด์ง,
์ฝ๊ด ํ์ด์ง
| ๊ณตํต ์ปดํฌ๋ํธ
(์ธํ, ๋ชจ๋ฌ)
ํ๋กํ ํ์ด์ง
๊ณต์ง์ฌํญ ํ์ด์ง
ํธ๋ ์ด๋ ํ์ด์ง
404 ํ์ด์ง | ๊ณตํต ์ปดํฌ๋ํธ
(์ ๋ต ๋ฆฌ์คํธ ์์ดํ
, ์ข
๋ชฉ&๋งค๋งค ์์ด์ฝ,
ํ
์ด๋ธ, ์ค์ผ๋ ํค, ํ์๊ฐ์
์คํ
,
์ ๋ต ์ ๋ณด, ๋ณ์ , ๊ฒ์๋ฐ, ์ฌ์ด๋ ์ ๋ณด,
๋ญํน ์ฐจํธ, ๋ถ์ ์ฐจํธ, ๋ชฉ๋ก ํค๋)
์ ๋ต ๋ญํน ๋ชจ์ ํ์ด์ง
์ ๋ต ์์ธ ํ์ด์ง | ํ๋ก์ ํธ ์ด๊ธฐ ์ธํ
(NextJS, ๋ฐฐํฌ, MSW)
๊ณตํต ์ปดํฌ๋ํธ
(์
๋ ํธ, ํค๋)
๊ด๋ฆฌ์ ๊ณต์ง ํ์ด์ง
๊ด๋ฆฌ์ ์ง๋ฌธ ํ์ด์ง
๊ด๋ฆฌ์ ์ฌ์ฉ์ ํ์ด์ง | ํ๋ก์ ํธ ์ด๊ธฐ ์ธํ
(Tanstack Query, MSW)
๊ณตํต ์ปดํฌ๋ํธ
(ํ์ด์ง๋ค์ด์
, ๋ฒํผ, ์ฒดํฌ ๋ฐ์ค, ๋๋ฉ ์ ์ฐจํธ)
๋ก๊ทธ์ธ ํ์ด์ง
๋์ ์ ๋ต ํ์ด์ง
์ ๋ต ๊ด๋ฆฌ ํ์ด์ง
์ ๋ต ๋ฑ๋ก ํ์ด์ง |
+
+
+
+## ํ์ด์ง ์๊ฐ
+
+
+
+ํ์๊ฐ์
๋ฒํผ์ ์๋จ์ ๋ฐฐ์นํ์ฌ ์ฆ๊ฐ์ ์ธ ๊ฐ์
์ ์ ๋ํ๋ฉฐ, ์ฌ์ดํธ ์ด์ฉ์ ์, ์ธ๊ธฐ ์ ๋ต, ํตํฉ ์งํ(SM Score) ๋ฑ ์ฃผ์ ์ ๋ณด๋ฅผ ํ๋์ ํ์ธํ ์ ์์ต๋๋ค.
+
+
+
+์ ๋ต์ ๋ญํน ์์ผ๋ก ํ์ธํ ์ ์์ผ๋ฉฐ ๋ก๊ทธ์ธํ์ง ์์ ์ฌ์ฉ์๋ ์ ๋ต ๋ชฉ๋ก์ ์์ ๋กญ๊ฒ ๋๋ฌ๋ณผ ์ ์์ต๋๋ค. ๊ฒ์๋ฐ๋ฅผ ํตํด ๋งค๋งค ์ ํ, SM Score ๋ฑ ๋ค์ํ ์กฐ๊ฑด์ผ๋ก ์ ๋ต์ ์ฝ๊ฒ ์ฐพ์๋ณผ ์ ์์ต๋๋ค.
+
+
+
+ํธ๋ ์ด๋ ์์ธ๋ณด๊ธฐ ํ์ด์ง์์ ํธ๋ ์ด๋์ ์ ๋ต์ ํ์ธํ ์ ์์ต๋๋ค.
+
+
+
+๋์ ์ ๋ต์ ํ์ธํ ์ ์์ผ๋ฉฐ, ๋ฌดํ ์คํฌ๋กค๋ก ๋ฐ์ดํฐ๋ฅผ ๋๊น ์์ด ํ์ํ ์ ์์ต๋๋ค.
+
+
+
+๋ด๊ฐ ๊ตฌ๋
ํ ์ ๋ต๋ค์ ํ๋์ ํ์ธํ ์ ์์ต๋๋ค.
+
+
+
+๋ฌธ์ ๋ด์ญ์ ํ์ธํ ์ ์์ผ๋ฉฐ, ๋ชจ๋ ๋ต๋ณ, ๋ต๋ณ ๋๊ธฐ, ๋ต๋ณ ์๋ฃ ์ํ๋ก ๊ตฌ๋ถ๋ฉ๋๋ค. ์ ๋ ฌ๊ณผ ๊ฒ์ ๊ธฐ๋ฅ์ ํตํด ์ํ๋ ๋ฐ์ดํฐ๋ฅผ ์ฝ๊ฒ ์ฐพ์ ์ ์์ต๋๋ค.
+
+
+
+๊ด๋ฆฌ์ ๊ณ์ ์ผ๋ก ๋ก๊ทธ์ธํ๋ฉด ํ์ ๊ด๋ฆฌ, ๊ณต์ง์ฌํญ ๋ฑ๋ก, ์ข
๋ชฉ ๋ฐ ๋งค๋งค ์ ํ ๊ด๋ฆฌ, ์ ๋ต ์น์ธ ๊ด๋ฆฌ, ๋ฌธ์ ๋ด์ญ ํ์ธ ๋ฑ์ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ฉฐ, ํจ์จ์ ์ธ ์ฌ์ดํธ ์ด์์ ์ง์ํฉ๋๋ค.
+
+## ๐ ๊ธฐ์ ์คํ
+
+| ๊ธฐ์ ์คํ | ๋์
์ด์ |
+| -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
+|

| (14.2.16) SSR๋ก SEO์ ์ด๊ธฐ ๋ก๋ฉ ์๋ ๊ฐ์ , ํด๋ ๊ธฐ๋ฐ ๋ผ์ฐํ
์ผ๋ก ๊ฒฝ๋ก ์๋ ์์ฑ |
+|

| (8.4.0) ๋ฌธ์ํ๋ก ์ฌ์ฉ ๋ฐฉ๋ฒ ๋ฐ ๋์์ธ ์์คํ
ํ์ธ, UI ๋ณ๊ฒฝ์ฌํญ์ ์ฆ๊ฐ ํ์ธํ๋ฉฐ ํ
์คํธ ์ฝ๋ ์๋ต ๊ฐ๋ฅ |
+|

| (1.80.5) Mixin์ผ๋ก ๋ฐ๋ณต ์คํ์ผ ์ฌ์ฌ์ฉ ํจ์จ์ฑ ์ฆ๋, ๋ณ์ ์ง์์ผ๋ก ์์ยทํฐํธ ๋ฑ ๊ณตํต ๊ฐ ๊ด๋ฆฌ ์ฉ์ด |
+|  | (5.0) ์ ์ ํ์
๊ฒ์ฌ๋ก ์์ ์ฑ ํ๋ณด ๋ฐ ๋ฐํ์ ์๋ฌ ๊ฐ์, ์ฝ๋ ์๋์์ฑ๊ณผ ๋ช
ํํ ํ์
์ ์๋ก ๊ฐ๋
์ฑ๊ณผ ์ ์ง๋ณด์์ฑ ํฅ์ ๊ด๋ฆฌ |
+|  | (18) ์ปดํฌ๋ํธ ๊ธฐ๋ฐ ์ํคํ
์ฒ๋ก ์ฌ์ฌ์ฉ์ฑ ๊ทน๋ํ, ์ ์ธํ UI๋ก ์ง๊ด์ ์ด๊ณ ํจ์จ์ ์ธ ๊ฐ๋ฐ ๊ฒฝํ ์ ๊ณต |
+|  | (5.59.19) ๋น๋๊ธฐ ์ํ ๊ด๋ฆฌ์ ์บ์ฑ์ผ๋ก ๋ฐ์ดํฐ ์์ฒญ ์ต์ ํ ๋ฐ ์๋ฒ ์ํ ๊ด๋ฆฌ ๊ฐ์ํ |
+|  | (5.0.1) ๊ฐ๋ฒผ์ด ์ํ ๊ด๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก ์ง๊ด์ ์ธ API์ ๋ถ๋ณ์ฑ ์์ด๋ ํจ์จ์ ์ธ ์ํ ๊ด๋ฆฌ ์ ๊ณต |
+
+
+
+## ํด๋ ๊ตฌ์กฐ
+
+
+
+#### FSD(Feature Sliced Design)์ ์ฅ์
+
+**1. ๋ชจ๋ํ์ ๋
๋ฆฝ์ฑ:** ๊ธฐ๋ฅ๋ณ๋ก ํ์ผ์ ๊ด๋ฆฌํด ์์ , ์ญ์ , ์ถ๊ฐ๊ฐ ์ฉ์ด
+**2. ๋ช
ํํ ๊ตฌ์กฐ:** ๊ธฐ๋ฅ ๋จ์๋ก ํด๋๊ฐ ๊ตฌ์ฑ๋์ด ํ์ผ์ ์ฝ๊ฒ ์ฐพ์ ์ ์์
+**3. ํ์
ํจ์จ์ฑ:** ์์
์์ญ์ด ๋ถ๋ฆฌ๋์ด ์ถฉ๋ ์์ด ๋์ ์์
๊ฐ๋ฅ
+**4. ์ ์ง๋ณด์ ์ฉ์ด์ฑ:** ๊ด๋ จ ์ฝ๋๊ฐ ํ ํด๋์ ๋ชจ์ฌ ์์ด ์์ ๋ฐ ํ์ฅ์ด ์ฌ์
+
+## ํ์๋ผ์ธ
+
+
diff --git a/app/(dashboard)/_ui/analysis-container/account-content.tsx b/app/(dashboard)/_ui/analysis-container/account-content.tsx
index 5b157a51..639641b1 100644
--- a/app/(dashboard)/_ui/analysis-container/account-content.tsx
+++ b/app/(dashboard)/_ui/analysis-container/account-content.tsx
@@ -8,6 +8,7 @@ import classNames from 'classnames/bind'
import { ACCOUNT_PAGE_COUNT } from '@/shared/constants/count-per-page'
import useModal from '@/shared/hooks/custom/use-modal'
+import { ImageDataModel } from '@/shared/types/strategy-data'
import { Button } from '@/shared/ui/button'
import Checkbox from '@/shared/ui/check-box'
import AccountImageModal from '@/shared/ui/modal/account-image-modal'
@@ -22,12 +23,6 @@ import styles from './styles.module.scss'
const cx = classNames.bind(styles)
-export interface ImageDataModel {
- id: number
- imageUrl: string
- title: string
-}
-
interface Props {
strategyId: number
currentPage: number
@@ -80,12 +75,12 @@ const AccountContent = ({ strategyId, currentPage, onPageChange, isEditable = fa
imageIds: selectedImages,
})
setSelectedImages([])
- } catch (error) {
- console.error('Failed to delete images:', error)
+ } catch (err) {
+ console.error('Failed to delete images:', err)
}
}
- if (!data || !Array.isArray(data.content) || isLoading) return null
+ if (!Array.isArray(data?.content) || isLoading) return null
const imagesData = data.content
const croppedImagesData: ImageDataModel[] = sliceArray(
@@ -95,6 +90,7 @@ const AccountContent = ({ strategyId, currentPage, onPageChange, isEditable = fa
)
const isTwoLines = (croppedImagesData?.length || 0) > 4
+
return (
{isEditable && (
@@ -118,7 +114,7 @@ const AccountContent = ({ strategyId, currentPage, onPageChange, isEditable = fa
)}
- {croppedImagesData && croppedImagesData.length !== 0 ? (
+ {croppedImagesData?.length > 0 ? (
<>
{croppedImagesData?.map((imageData: ImageDataModel) => (
diff --git a/app/(dashboard)/_ui/analysis-container/analysis-chart.tsx b/app/(dashboard)/_ui/analysis-container/analysis-chart.tsx
index 8eff7139..dd8785b5 100644
--- a/app/(dashboard)/_ui/analysis-container/analysis-chart.tsx
+++ b/app/(dashboard)/_ui/analysis-container/analysis-chart.tsx
@@ -5,8 +5,8 @@ import dynamic from 'next/dynamic'
import classNames from 'classnames/bind'
import Highcharts, { SeriesOptionsType } from 'highcharts'
+import { CHART_SELECT_OPTIONS } from './constants'
import styles from './styles.module.scss'
-import { YAXIS_OPTIONS } from './yaxis-options'
const HighchartsReact = dynamic(() => import('highcharts-react-official'), {
ssr: false,
@@ -14,7 +14,7 @@ const HighchartsReact = dynamic(() => import('highcharts-react-official'), {
const cx = classNames.bind(styles)
-type YAxisType = keyof typeof YAXIS_OPTIONS
+type YAxisType = keyof typeof CHART_SELECT_OPTIONS
interface AnalysisChartDataModel {
dates: string[]
@@ -30,23 +30,23 @@ interface Props {
const AnalysisChart = ({ analysisChartData: data }: Props) => {
const getOptionName = (sequence: number) => {
const key = Object.keys(data.data)[sequence] as YAxisType | undefined
- return key ? YAXIS_OPTIONS[key] : ''
+ return key ? CHART_SELECT_OPTIONS[key] : ''
}
+
if (!data) return
+
const chartOptions: Highcharts.Options = {
chart: {
type: 'areaspline',
height: 367,
backgroundColor: 'transparent',
- margin: [10, 60, 10, 60],
+ margin: [10, 75, 10, 75],
zoomType: 'x',
} as Highcharts.ChartOptions,
title: { text: undefined },
xAxis: {
visible: false,
categories: data.dates,
- min: data.dates.length > 30 ? data.dates.length - 30 : 0,
- max: data.dates.length - 1,
},
yAxis: [
{
@@ -86,7 +86,7 @@ const AnalysisChart = ({ analysisChartData: data }: Props) => {
align: 'left',
verticalAlign: 'top',
layout: 'vertical',
- x: 40,
+ x: 70,
y: -10,
itemStyle: {
color: '#4D4D4D',
diff --git a/app/(dashboard)/_ui/analysis-container/analysis-content.tsx b/app/(dashboard)/_ui/analysis-container/analysis-content.tsx
index 43b53475..c7a19a5e 100644
--- a/app/(dashboard)/_ui/analysis-container/analysis-content.tsx
+++ b/app/(dashboard)/_ui/analysis-container/analysis-content.tsx
@@ -3,40 +3,27 @@ import { useState } from 'react'
import classNames from 'classnames/bind'
import { ANALYSIS_PAGE_COUNT } from '@/shared/constants/count-per-page'
+import useDelayedLoading from '@/shared/hooks/custom/use-delayed-loading'
import useModal from '@/shared/hooks/custom/use-modal'
+import { MyDailyAnalysisModel } from '@/shared/types/strategy-data'
import { Button } from '@/shared/ui/button'
import AnalysisUploadModal from '@/shared/ui/modal/analysis-upload-modal'
+import DailyAnalysisDeleteAllModal from '@/shared/ui/modal/daily-analysis-delete-all-modal'
+import EditAnalysisModal from '@/shared/ui/modal/edit-daily-analysis-modal.ts'
import Pagination from '@/shared/ui/pagination'
-import VerticalTable from '@/shared/ui/table/vertical'
+import VerticalTable, { TableBodyDataType } from '@/shared/ui/table/vertical'
import { useAnalysisUploadMutation } from '../../my/_hooks/query/use-analysis-mutation'
import useGetMyDailyAnalysis from '../../my/_hooks/query/use-get-my-daily-analysis'
+import { useMyAnalysisMutation } from '../../my/_hooks/query/use-manage-daily-analysis'
import useGetAnalysis from '../../strategies/[strategyId]/_hooks/query/use-get-analysis'
import useGetAnalysisDownload from '../../strategies/[strategyId]/_hooks/query/use-get-analysis-download'
+import { generateFormattedStrategyData } from '../../strategies/[strategyId]/util'
+import { DAILY_TABLE_HEADER, MONTHLY_TABLE_HEADER } from './constants'
import styles from './styles.module.scss'
const cx = classNames.bind(styles)
-const DAILY_TABLE_HEADER = [
- '๋ ์ง',
- '์๊ธ',
- '์
์ถ๊ธ',
- '์ผ ์์ต',
- '์ผ ์์ต๋ฅ ',
- '๋์ ์์ต',
- '๋์ ์์ต๋ฅ ',
-]
-
-const MONTHLY_TABLE_HEADER = [
- '๋ ์ง',
- '์๊ธ',
- '์
์ถ๊ธ',
- '์ ์์ต',
- '์ ์์ต๋ฅ ',
- '๋์ ์์ต',
- '๋์ ์์ต๋ฅ ',
-]
-
interface Props {
type: 'daily' | 'monthly'
strategyId: number
@@ -45,6 +32,18 @@ interface Props {
isEditable?: boolean
}
+const isMyAnalysisData = (data: TableBodyDataType): data is MyDailyAnalysisModel => {
+ if (!data || typeof data !== 'object' || Array.isArray(data)) return false
+
+ return (
+ 'dailyAnalysisId' in data &&
+ 'dailyDate' in data &&
+ 'transaction' in data &&
+ 'dailyProfitLoss' in data &&
+ 'principal' in data
+ )
+}
+
const AnalysisContent = ({
type,
strategyId,
@@ -52,18 +51,29 @@ const AnalysisContent = ({
onPageChange,
isEditable = false,
}: Props) => {
+ const COLWIDTH = [1.7, 1.7, 1.3, 1.5, 1, 1.5, 1]
const { mutate } = useGetAnalysisDownload()
-
const [uploadType, setUploadType] = useState<'excel' | 'direct' | null>(null)
+ const [selectedAnalysis, setSelectedAnalysis] = useState
(null)
+
const { isModalOpen, openModal, closeModal } = useModal()
+ const {
+ isModalOpen: isDeleteModalOpen,
+ openModal: openDeleteModal,
+ closeModal: closeDeleteModal,
+ } = useModal()
+ const {
+ isModalOpen: isEditModalOpen,
+ openModal: openEditModal,
+ closeModal: closeEditModal,
+ } = useModal()
- //TODO ํ์ฌ ๋์ ์ ๋ต ์ผ๊ฐ๋ถ์ ์กฐํ ๊ถํ์ด ์์ด์ ์๋ณด์
- const { data: myAnalysisData } = useGetMyDailyAnalysis(
+ const { data: myAnalysisData, isLoading: isMyAnalysisLoading } = useGetMyDailyAnalysis(
strategyId,
currentPage,
ANALYSIS_PAGE_COUNT
)
- const { data: publicAnalysisData } = useGetAnalysis(
+ const { data: publicAnalysisData, isLoading: isPublicAnalysisLoading } = useGetAnalysis(
strategyId,
type,
currentPage,
@@ -71,19 +81,22 @@ const AnalysisContent = ({
)
const analysisData = isEditable ? myAnalysisData : publicAnalysisData
+ const isLoading = isEditable ? isMyAnalysisLoading : isPublicAnalysisLoading
+ const isDelayedLoading = useDelayedLoading(isLoading, 500)
- const { deleteAllAnalysis, isLoading } = useAnalysisUploadMutation(
+ const analysisContent = generateFormattedStrategyData(analysisData?.content)
+
+ const { deleteAllAnalysis, isLoading: isDeleteAllLoading } = useAnalysisUploadMutation(
strategyId,
currentPage,
ANALYSIS_PAGE_COUNT
)
+ const { deleteAnalysisData } = useMyAnalysisMutation(strategyId, currentPage, ANALYSIS_PAGE_COUNT)
const handleDownload = () => {
mutate({ strategyId, type })
}
- const tableHeader = type === 'daily' ? DAILY_TABLE_HEADER : MONTHLY_TABLE_HEADER
-
const handleExcelUpload = () => {
setUploadType('excel')
openModal()
@@ -99,20 +112,61 @@ const AnalysisContent = ({
setUploadType(null)
}
+ const handleCloseEditModal = () => {
+ closeEditModal()
+ setSelectedAnalysis(null)
+ }
+
const handleDeleteAll = async () => {
- if (window.confirm('๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ์ญ์ ํ์๊ฒ ์ต๋๊น?')) {
- try {
- await deleteAllAnalysis()
- } catch (error) {
- console.error('Delete error:', error)
- alert('๋ฐ์ดํฐ ์ญ์ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.')
- }
+ try {
+ await deleteAllAnalysis()
+ closeDeleteModal()
+ } catch (err) {
+ console.error('Delete error:', err)
}
}
+ const handleDeleteAnalysis = async (dailyAnalysisId: number) => {
+ try {
+ await deleteAnalysisData(dailyAnalysisId)
+ } catch (err) {
+ console.error('Delete failed:', err)
+ }
+ }
+
+ const renderActions = (row: TableBodyDataType) => {
+ if (!isMyAnalysisData(row)) return null
+
+ return (
+
+
+
+
+ )
+ }
+
+ const tableHeader = type === 'daily' ? DAILY_TABLE_HEADER : MONTHLY_TABLE_HEADER
+
return (
-
- {!isEditable && analysisData && (
+
+ {!isEditable && analysisData?.content.length > 0 && (
-
)}
- {analysisData ? (
+ {isDelayedLoading ? (
+
+ ) : analysisData?.content.length > 0 ? (
<>
)}
+
+ {selectedAnalysis && (
+
+ )}
+
+
)
}
diff --git a/app/(dashboard)/_ui/analysis-container/yaxis-options.ts b/app/(dashboard)/_ui/analysis-container/constants.ts
similarity index 65%
rename from app/(dashboard)/_ui/analysis-container/yaxis-options.ts
rename to app/(dashboard)/_ui/analysis-container/constants.ts
index a9be1ddf..38b37f2c 100644
--- a/app/(dashboard)/_ui/analysis-container/yaxis-options.ts
+++ b/app/(dashboard)/_ui/analysis-container/constants.ts
@@ -1,4 +1,4 @@
-export const YAXIS_OPTIONS = {
+export const CHART_SELECT_OPTIONS = {
BALANCE: '์๊ณ ',
PRINCIPAL: '์๊ธ',
CUMULATIVE_TRANSACTION_AMOUNT: '๋์ ์
์ถ ๊ธ์ก',
@@ -17,3 +17,23 @@ export const YAXIS_OPTIONS = {
TOTAL_PROFIT: '์ด ์ด์ต',
TOTAL_LOSS: '์ด ์์ค',
} as const
+
+export const DAILY_TABLE_HEADER = [
+ '๋ ์ง',
+ '์๊ธ',
+ '์
์ถ๊ธ',
+ '์ผ ์์ต',
+ '์ผ ์์ต๋ฅ ',
+ '๋์ ์์ต',
+ '๋์ ์์ต๋ฅ ',
+]
+
+export const MONTHLY_TABLE_HEADER = [
+ '๋ ์ง',
+ '์๊ธ',
+ '์
์ถ๊ธ',
+ '์ ์์ต',
+ '์ ์์ต๋ฅ ',
+ '๋์ ์์ต',
+ '๋์ ์์ต๋ฅ ',
+]
diff --git a/app/(dashboard)/_ui/analysis-container/example.ts b/app/(dashboard)/_ui/analysis-container/example.ts
deleted file mode 100644
index c1b1289d..00000000
--- a/app/(dashboard)/_ui/analysis-container/example.ts
+++ /dev/null
@@ -1,123 +0,0 @@
-export const analysisChartData = {
- dates: ['2023-01-01', '2023-01-02', '2023-01-03', '2023-01-04', '2023-01-05', '2023-01-06'],
- data: {
- CURRENT_DRAWDOWN: [2000, 5660, 4000, 9000, 7000, 10000],
- PRINCIPAL: [50000, 60000, 80000, 70000, 80000, 90000],
- },
-}
-
-export const statisticsData = {
- assetManagement: {
- balance: 896217437, // ์๊ณ
- cumulativeTransactionAmount: 896217437, // ๋์ ๊ฑฐ๋ ๊ธ์ก
- principal: 238704360, // ์๊ธ
- operationPeriod: '2๋
4์', // ์ด์ฉ ๊ธฐ๊ฐ
- startDate: '2012-10-11', // ์์ ์ผ์
- endDate: '2015-03-11', // ์ข
๋ฃ ์ผ์ (endDate)
- daysSincePeakUpdate: 513, // ๊ณ ์ ๊ฐฑ์ ํ ๊ฒฝ๊ณผ์ผ
- },
- profitLoss: {
- cumulativeProfitAmount: 247525031, // ๋์ ์์ต ๊ธ์ก
- cumulativeProfitRate: 49.24, // ๋์ ์์ต๋ฅ
- maxCumulativeProfitAmount: 247525031, // ์ต๋ ๋์ ์์ต ๊ธ์ก
- maxCumulativeProfitRate: 49.24, // ์ต๋ ๋์ ์์ต๋ฅ
- averageProfitLossAmount: 336311, // ํ๊ท ์์ต ๊ธ์ก
- averageProfitLossRate: 6, // ํ๊ท ์์ต๋ฅ
- maxDailyProfitAmount: 25257250, // ์ต๋ ์ผ ์์ต ๊ธ์ก
- maxDailyProfitRate: 3.985, // ์ต๋ ์ผ ์์ต๋ฅ
- maxDailyLossAmount: -17465050, // ์ต๋ ์ผ ์์ค ๊ธ์ก
- maxDailyLossRate: -3.95, // ์ต๋ ์ผ ์์ค๋ฅ
- roa: 453, // ์์ฐ ์์ต๋ฅ (Return on Assets)
- profitFactor: 1.48, // Profit Factor
- },
- ddMddInfo: {
- currentDrawdown: 0, // ํ์ฌ ์๋ณธ ์ธํ ๊ธ์ก
- currentDrawdownRate: 0, // ํ์ฌ ์๋ณธ ์ธํ์จ
- maxDrawdown: -54832778, // ์ต๋ ์๋ณธ ์ธํ ๊ธ์ก
- maxDrawdownRate: -13.98, // ์ต๋ ์๋ณธ ์ธํ์จ
- },
- tradingInfo: {
- totalTradeDays: 736, // ์ด ๊ฑฐ๋ ์ผ์
- totalProfitableDays: 508, // ์ด ์ด์ต ์ผ์
- totalLossDays: 228, // ์ด ์์ค ์ผ์
- currentConsecutiveLossDays: 6, // ํ์ฌ ์ฐ์ ์์ค ์ผ์
- maxConsecutiveProfitDays: 22, // ์ต๋ ์ฐ์ ์ด์ต ์ผ์
- maxConsecutiveLossDays: 8, // ์ต๋ ์ฐ์ ์์ค ์ผ์
- winRate: 69, // ์น๋ฅ
- },
-}
-
-export const tableBody = [
- {
- date: '2015-03-12', // ๋ ์ง
- principal: 100000000, // ์๊ธ
- transaction: 0, // ์
์ถ๊ธ
- dailyProfitLoss: 332410, // ์ผ ์์ต
- dailyProfitLossRate: 0.33, // ์ผ ์์ต๋ฅ
- cumulativeProfitLoss: 302280, // ๋์ ์์ต
- cumulativeProfitLossRate: 0.3, // ๋์ ์์ต๋ฅ
- },
- {
- date: '2015-03-13',
- principal: 100000000,
- transaction: 0,
- dailyProfitLoss: 332410,
- dailyProfitLossRate: 0.33,
- cumulativeProfitLoss: 302280,
- cumulativeProfitLossRate: 0.3,
- },
- {
- date: '2015-03-14',
- principal: 100000000,
- transaction: 0,
- dailyProfitLoss: 332410,
- dailyProfitLossRate: 0.33,
- cumulativeProfitLoss: 302280,
- cumulativeProfitLossRate: 0.3,
- },
- {
- date: '2015-03-15',
- principal: 100000000,
- transaction: 0,
- dailyProfitLoss: 332410,
- dailyProfitLossRate: 0.33,
- cumulativeProfitLoss: 302280,
- cumulativeProfitLossRate: 0.3,
- },
- {
- date: '2015-03-16',
- principal: 100000000,
- transaction: 0,
- dailyProfitLoss: 332410,
- dailyProfitLossRate: 0.33,
- cumulativeProfitLoss: 302280,
- cumulativeProfitLossRate: 0.3,
- },
- {
- date: '2015-03-17',
- principal: 100000000,
- transaction: 0,
- dailyProfitLoss: 332410,
- dailyProfitLossRate: 0.33,
- cumulativeProfitLoss: 302280,
- cumulativeProfitLossRate: 0.3,
- },
- {
- date: '2015-03-18',
- principal: 100000000,
- transaction: 0,
- dailyProfitLoss: 332410,
- dailyProfitLossRate: 0.33,
- cumulativeProfitLoss: 302280,
- cumulativeProfitLossRate: 0.3,
- },
- {
- date: '2015-03-19',
- principal: 100000000,
- transaction: 0,
- dailyProfitLoss: 332410,
- dailyProfitLossRate: 0.33,
- cumulativeProfitLoss: 302280,
- cumulativeProfitLossRate: 0.3,
- },
-]
diff --git a/app/(dashboard)/_ui/analysis-container/index.tsx b/app/(dashboard)/_ui/analysis-container/index.tsx
index c6422eff..e3b74cfc 100644
--- a/app/(dashboard)/_ui/analysis-container/index.tsx
+++ b/app/(dashboard)/_ui/analysis-container/index.tsx
@@ -8,13 +8,13 @@ import Select from '@/shared/ui/select'
import useGetAnalysisChart from '../../strategies/[strategyId]/_hooks/query/use-get-analysis-chart'
import AnalysisChart from './analysis-chart'
+import { CHART_SELECT_OPTIONS } from './constants'
import styles from './styles.module.scss'
import TabsWithTable from './tabs-width-table'
-import { YAXIS_OPTIONS } from './yaxis-options'
const cx = classNames.bind(styles)
-export type AnalysisChartOptionsType = keyof typeof YAXIS_OPTIONS
+export type AnalysisChartOptionsType = keyof typeof CHART_SELECT_OPTIONS
interface Props {
strategyId: number
@@ -27,12 +27,9 @@ const AnalysisContainer = ({ strategyId, type = 'default' }: Props) => {
useState
('CUMULATIVE_PROFIT_LOSS')
const { data: chartData } = useGetAnalysisChart({ strategyId, firstOption, secondOption })
- const optionsToArray = Object.entries(YAXIS_OPTIONS)
- const options: { value: string; label: string }[] = []
-
- for (const [key, value] of optionsToArray) {
- options.push({ value: key, label: value })
- }
+ const options = Object.entries(CHART_SELECT_OPTIONS).map((option) => {
+ return { value: option[0], label: option[1] }
+ })
return (
diff --git a/app/(dashboard)/_ui/analysis-container/statistics-content.tsx b/app/(dashboard)/_ui/analysis-container/statistics-content.tsx
index b70db4fb..85d3befe 100644
--- a/app/(dashboard)/_ui/analysis-container/statistics-content.tsx
+++ b/app/(dashboard)/_ui/analysis-container/statistics-content.tsx
@@ -14,7 +14,7 @@ interface StatisticsDataModel {
}
interface Props {
- statisticsData: StatisticsDataModel
+ statisticsData?: StatisticsDataModel
}
const StatisticsContent = ({ statisticsData }: Props) => {
diff --git a/app/(dashboard)/_ui/analysis-container/styles.module.scss b/app/(dashboard)/_ui/analysis-container/styles.module.scss
index e139a8e9..a5d5f9df 100644
--- a/app/(dashboard)/_ui/analysis-container/styles.module.scss
+++ b/app/(dashboard)/_ui/analysis-container/styles.module.scss
@@ -2,6 +2,7 @@
padding: 20px;
border-radius: 5px;
background-color: $color-white;
+
.analysis-header {
display: flex;
justify-content: space-between;
@@ -29,21 +30,29 @@
.table-wrapper {
margin-top: 20px;
position: relative;
+
.edit-button-container {
display: flex;
justify-content: space-between;
margin-bottom: 30px;
+
.edit-button,
.delete-button {
padding: 7px 18px;
}
+
.delete-button:disabled {
background-color: transparent;
}
}
+
&.analysis {
margin-top: 40px;
}
+ &.my-analysis {
+ margin-top: 20px;
+ }
+
.excel-button {
height: 30px;
position: absolute;
@@ -57,15 +66,18 @@
grid-template-columns: repeat(5, 1fr);
grid-template-rows: repeat(1, 1fr);
column-gap: 20px;
+
&.line {
grid-template-rows: repeat(2, 1fr);
row-gap: 10px;
}
+
.image-data {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
+
.image {
position: relative;
width: 100%;
@@ -74,11 +86,13 @@
cursor: pointer;
overflow: hidden;
}
+
span {
text-align: center;
@include typo-c1;
}
}
+
.title-wrapper {
display: flex;
justify-content: center;
@@ -100,13 +114,23 @@
.button-container {
display: flex;
justify-content: space-between;
+ align-items: center;
height: 30px;
- margin-top: -20px;
- margin-bottom: 10px;
+ gap: 8px;
+ padding: 0 10px;
- button.upload-button {
- height: 100%;
+ button.upload-button,
+ button.delete-all-button {
padding: 7px 18px;
margin-right: 10px;
}
+
+ button.delete-button,
+ button.edit-button {
+ min-width: 60px;
+ height: 24px;
+ padding: 7px 16px;
+ border-radius: 16px;
+ border: 1px solid $color-gray-300;
+ }
}
diff --git a/app/(dashboard)/_ui/details-information/index.tsx b/app/(dashboard)/_ui/details-information/index.tsx
index 3aae89cd..5a08238e 100644
--- a/app/(dashboard)/_ui/details-information/index.tsx
+++ b/app/(dashboard)/_ui/details-information/index.tsx
@@ -14,9 +14,17 @@ interface Props {
strategyId: number
information: StrategyDetailsInformationModel
type?: 'default' | 'my'
+ isEditable?: boolean
+ error?: Error
}
-const DetailsInformation = ({ strategyId, information, type = 'default' }: Props) => {
+const DetailsInformation = ({
+ strategyId,
+ information,
+ type = 'default',
+ error,
+ isEditable = false,
+}: Props) => {
const percentageToArray = [
{ percent: information.cumulativeProfitRate, label: '๋์ ์์ต๋ฅ ' },
{ percent: information.maxDrawdownRate, label: '์ต๋ ์๋ณธ ์ธํ์จ' },
@@ -29,6 +37,7 @@ const DetailsInformation = ({ strategyId, information, type = 'default' }: Props
<>
-
+
{type === 'default' && (
{percentageToArray.map((data) => (
diff --git a/app/(dashboard)/_ui/details-information/invest-information.tsx b/app/(dashboard)/_ui/details-information/invest-information.tsx
index 9a9a78a8..5a51b8cf 100644
--- a/app/(dashboard)/_ui/details-information/invest-information.tsx
+++ b/app/(dashboard)/_ui/details-information/invest-information.tsx
@@ -8,16 +8,17 @@ interface Props {
stock: string[]
trade: string
cycle: string
+ isEditable?: boolean
}
-const InvestInformation = ({ stock, trade, cycle }: Props) => {
+const InvestInformation = ({ stock, trade, cycle, isEditable = false }: Props) => {
const investData = [
{ title: 'ํฌ์ ์ข
๋ชฉ', data: stock.join(',') },
{ title: '๋งค๋งค ์ ํ', data: trade },
{ title: 'ํฌ์ ์ฃผ๊ธฐ', data: cycle },
]
return (
-
+
{investData.map((data, idx) => (
{data.title}
diff --git a/app/(dashboard)/_ui/details-information/strategy-name-box.tsx b/app/(dashboard)/_ui/details-information/strategy-name-box.tsx
index ff29ebe5..08b509c4 100644
--- a/app/(dashboard)/_ui/details-information/strategy-name-box.tsx
+++ b/app/(dashboard)/_ui/details-information/strategy-name-box.tsx
@@ -1,8 +1,17 @@
-import React from 'react'
+'use client'
+
+import { useEffect, useRef, useState } from 'react'
import StrategiesIcon from '@/app/(dashboard)/_ui/strategies-item/strategies-icon'
+import { FileIcon } from '@/public/icons'
+import { TrashcanIcon } from '@/public/icons'
import classNames from 'classnames/bind'
+import { SUPPORTED_FILE_TYPES } from '@/shared/constants/supported-file-types'
+import Input from '@/shared/ui/input'
+
+import useGetProposalFileName from '../../my/_hooks/query/use-get-proposal-file-name'
+import useEditInformationStore from '../../my/strategies/manage/[strategyId]/_store/use-edit-information-store'
import useGetProposalDownload from '../../strategies/[strategyId]/_hooks/query/use-get-proposal-download'
import styles from './styles.module.scss'
@@ -10,23 +19,132 @@ const cx = classNames.bind(styles)
interface Props {
strategyId: number
+ name: string
+ hasProposal: boolean
iconUrls?: string[]
iconNames?: string[]
- name: string
+ isEditable?: boolean
+ error?: Error
}
-const StrategyNameBox = ({ strategyId, iconUrls, iconNames, name }: Props) => {
+const StrategyNameBox = ({
+ strategyId,
+ name,
+ hasProposal: initialHasProposal,
+ iconUrls,
+ iconNames,
+ isEditable = false,
+ error,
+}: Props) => {
+ const information = useEditInformationStore((state) => state.information)
+ const proposal = useEditInformationStore((state) => state.proposal)
+ const setStrategyName = useEditInformationStore((state) => state.actions.setStrategyName)
+ const setProposalFile = useEditInformationStore((state) => state.actions.setProposalFile)
+ const initializeProposal = useEditInformationStore((state) => state.actions.initializeProposal)
+ const setProposalModified = useEditInformationStore((state) => state.actions.setProposalModified)
+
+ const { refetch } = useGetProposalFileName(strategyId)
const { mutate } = useGetProposalDownload()
+ const fileInputRef = useRef
(null)
+ const [selectedFile, setSelectedFile] = useState(null)
+ const [fileError, setFileError] = useState('')
const handleDownload = () => {
mutate({ strategyId, name })
}
+ const handleFileChange = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0]
+ const supportedTypes = SUPPORTED_FILE_TYPES
+
+ if (file && supportedTypes.includes(file.type)) {
+ setSelectedFile(file)
+ setProposalFile(file)
+ setFileError('')
+ } else {
+ setSelectedFile(null)
+ setProposalFile(null)
+ setFileError('์ง์๋๋ ํ์ผ ํ์์ด ์๋๋๋ค.')
+ }
+ }
+
+ const handleProposalClick = () => {
+ if (fileInputRef.current) {
+ fileInputRef.current.click()
+ }
+ }
+
+ const handleProposalDelete = () => {
+ if (selectedFile || proposal.proposalFileName) {
+ setSelectedFile(null)
+ setProposalFile(null)
+ initializeProposal('')
+ setProposalModified(true)
+ setFileError('')
+ }
+ }
+
+ useEffect(() => {
+ setStrategyName(name)
+ }, [name, setStrategyName])
+
+ useEffect(() => {
+ if (isEditable) {
+ refetch().then((response) => {
+ const fileName = response.data?.result?.proposalFileName
+ if (fileName) {
+ initializeProposal(fileName)
+ }
+ })
+ }
+ }, [isEditable, refetch, initializeProposal])
+
+ const displayFileName =
+ selectedFile?.name || proposal.proposalFileName || '๋ฑ๋ก๋ ์ ์์๊ฐ ์์ต๋๋ค'
+
return (
-
-
{name}
-
์ ์์ ๋ค์ด๋ก๋
+
+
+ {isEditable ? (
+
) => setStrategyName(e.target.value)}
+ inputSize="small"
+ className={cx('name-input')}
+ maxLength={16}
+ value={information.strategyName as string}
+ />
+ ) : (
+
{name}
+ )}
+ {initialHasProposal && !isEditable && (
+
+ ์ ์์ ๋ค์ด๋ก๋
+
+ )}
+
+ {isEditable && (
+
+
+
+
+
+
+
+
+
+
+
+ {error &&
{error.message}
}
+ {fileError &&
{fileError}
}
+
+ )}
)
}
diff --git a/app/(dashboard)/_ui/details-information/styles.module.scss b/app/(dashboard)/_ui/details-information/styles.module.scss
index 46224894..4e0ebbbd 100644
--- a/app/(dashboard)/_ui/details-information/styles.module.scss
+++ b/app/(dashboard)/_ui/details-information/styles.module.scss
@@ -5,22 +5,100 @@
}
.name-container {
- width: 30%;
+ width: 35%;
height: 144px;
- padding: 20px;
+ padding: 18px 10px;
display: flex;
+ justify-content: center;
flex-direction: column;
- gap: 10px;
+ gap: 4px;
border-radius: 5px;
background-color: $color-white;
- .name {
- @include typo-h4;
+
+ .name-section {
+ .name-input {
+ padding: 10px;
+ height: 30px;
+ width: 45%;
+ margin: 6px 0;
+ color: $color-gray-700;
+ }
+ .name {
+ @include typo-b1;
+ margin: 10px 0;
+ }
+ button {
+ height: 30px;
+ width: 100%;
+ border-radius: 8px;
+ background-color: $color-gray-200;
+ color: $color-gray-700;
+ }
+ }
+
+ .proposal-section {
+ width: 100%;
+
+ .file-input {
+ display: none;
+ }
+
+ .proposal-input-wrapper {
+ position: relative;
+ width: 100%;
+
+ .file-name-input {
+ width: 100%;
+ height: 30px;
+ background-color: $color-gray-200;
+ border: 0;
+ border-radius: 8px;
+ cursor: default;
+ color: $color-gray-700;
+ padding-right: 70px;
+ text-overflow: ellipsis;
+ padding-left: 16px;
+ }
+
+ .proposal-button {
+ position: absolute;
+ right: 16px;
+ top: 50%;
+ transform: translateY(-50%);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: $color-gray-700;
+ background: none;
+ @include typo-b3;
+
+ svg {
+ width: 20px;
+ height: 20px;
+ }
+
+ &.modify {
+ right: 40px;
+ }
+
+ &.delete {
+ right: 12px;
+ }
+ }
+ }
+
+ .error-message {
+ @include typo-c1;
+ color: $color-orange-700;
+ margin-top: 8px;
+ }
}
- button {
- height: 36px;
- border-radius: 8px;
- background-color: $color-gray-100;
- color: $color-gray-700;
+
+ @include tablet-md {
+ gap: 2px;
+ button {
+ font-size: $text-b3;
+ }
}
}
@@ -29,14 +107,20 @@
height: 144px;
display: flex;
gap: 20px;
- padding: 30px;
+ padding: 20px;
border-radius: 5px;
background-color: $color-white;
+
+ &.edit {
+ background-color: $color-gray-200;
+ }
+
.info-item {
width: 100%;
height: 100%;
border-right: 1px solid $color-gray-200;
padding-right: 4px;
+ overflow-y: auto;
&:last-child {
border-right: 0;
}
@@ -48,6 +132,16 @@
@include typo-b3;
color: $color-gray-700;
margin-top: 16px;
+ @include tablet-md {
+ font-size: $text-c1;
+ }
+ }
+ &::-webkit-scrollbar {
+ width: 6px;
+ }
+ &::-webkit-scrollbar-thumb {
+ background-color: $color-gray-200;
+ border-radius: 10px;
}
}
}
@@ -62,6 +156,7 @@
width: 20%;
height: 108px;
border-radius: 5px;
+ gap: 8px;
background-color: $color-white;
display: flex;
flex-direction: column;
@@ -71,12 +166,24 @@
.label {
@include typo-b2;
color: $color-gray-800;
+ @include tablet-md {
+ font-size: $text-b3;
+ }
+ @include tablet-sm {
+ font-size: $text-c1;
+ }
}
.percent {
- @include typo-h3;
+ @include typo-h4;
color: #f53500;
&.minus {
color: #6877ff;
}
+ @include tablet-md {
+ font-size: $text-b1;
+ }
+ @include tablet-sm {
+ font-size: $text-b2;
+ }
}
}
diff --git a/app/(dashboard)/_ui/details-side-item/index.tsx b/app/(dashboard)/_ui/details-side-item/index.tsx
index c550f81a..75d520ac 100644
--- a/app/(dashboard)/_ui/details-side-item/index.tsx
+++ b/app/(dashboard)/_ui/details-side-item/index.tsx
@@ -25,12 +25,14 @@ interface Props {
profileImage?: string
isMyStrategy?: boolean
strategyName?: string
+ isEditable?: boolean
}
const DetailsSideItem = ({
strategyId,
information,
profileImage,
+ isEditable = false,
isMyStrategy = true,
strategyName,
}: Props) => {
@@ -38,12 +40,12 @@ const DetailsSideItem = ({
return (
<>
{isArray ? (
-
+
{information.map((item) => (
{item.title}
-
{item.data}
+
{typeof item.data === 'number' ? item.data.toFixed(2) : item.data}
))}
@@ -56,6 +58,7 @@ const DetailsSideItem = ({
profileImage={profileImage}
isMyStrategy={isMyStrategy}
strategyName={strategyName}
+ isEditable={isEditable}
/>
)}
>
diff --git a/app/(dashboard)/_ui/details-side-item/side-item.tsx b/app/(dashboard)/_ui/details-side-item/side-item.tsx
index 265bac8e..e132a209 100644
--- a/app/(dashboard)/_ui/details-side-item/side-item.tsx
+++ b/app/(dashboard)/_ui/details-side-item/side-item.tsx
@@ -1,10 +1,7 @@
'use client'
-import { usePathname, useRouter } from 'next/navigation'
-
import classNames from 'classnames/bind'
-import { PATH } from '@/shared/constants/path'
import useModal from '@/shared/hooks/custom/use-modal'
import { useAuthStore } from '@/shared/stores/use-auth-store'
import Avatar from '@/shared/ui/avatar'
@@ -25,6 +22,7 @@ interface Props {
profileImage?: string
isMyStrategy?: boolean
strategyName?: string
+ isEditable?: boolean
}
const SideItem = ({
@@ -34,6 +32,7 @@ const SideItem = ({
profileImage,
isMyStrategy = false,
strategyName,
+ isEditable = false,
}: Props) => {
const {
isModalOpen: isAddQuestionModalOpen,
@@ -46,17 +45,11 @@ const SideItem = ({
closeModal: guideCloseModal,
} = useModal()
const user = useAuthStore((state) => state.user)
- const router = useRouter()
- const path = usePathname()
-
- const handleRouter = () => {
- router.push(`${PATH.MY_STRATEGIES}/manage/${strategyId}`)
- }
const isTrader = user?.role.includes('TRADER')
return (
-
+
{title}
{title === 'ํธ๋ ์ด๋' ? (
@@ -66,15 +59,10 @@ const SideItem = ({
{data}
{!isMyStrategy && !isTrader && (
-
+
๋ฌธ์ํ๊ธฐ
)}
- {isMyStrategy && !path.includes('my') && (
-
- ๋ด ์ ๋ต ๊ด๋ฆฌํ๊ธฐ
-
- )}
>
) : (
{formatNumber(data)}
diff --git a/app/(dashboard)/_ui/details-side-item/styles.module.scss b/app/(dashboard)/_ui/details-side-item/styles.module.scss
index b4393c12..530e01ab 100644
--- a/app/(dashboard)/_ui/details-side-item/styles.module.scss
+++ b/app/(dashboard)/_ui/details-side-item/styles.module.scss
@@ -15,10 +15,15 @@
.side-item,
.side-items {
- width: 276px;
+ width: 100%;
background-color: $color-white;
border-radius: 5px;
margin-bottom: 20px;
+
+ &.edit {
+ background-color: $color-gray-200;
+ }
+
.title {
font-weight: $text-bold;
font-size: $text-b2;
@@ -42,5 +47,13 @@
align-items: center;
p {
margin: 0 4px 0 11px;
+ @include tablet-md {
+ margin: 0 2px 0 4px;
+ }
}
}
+
+.trader-button {
+ height: 30px;
+ padding: 0;
+}
diff --git a/app/(dashboard)/_ui/introduction/index.tsx b/app/(dashboard)/_ui/introduction/index.tsx
index b9ee51cd..6e578a34 100644
--- a/app/(dashboard)/_ui/introduction/index.tsx
+++ b/app/(dashboard)/_ui/introduction/index.tsx
@@ -1,27 +1,41 @@
'use client'
+/* eslint-disable react-hooks/exhaustive-deps */
import { useEffect, useRef, useState } from 'react'
import { CloseIcon, OpenIcon } from '@/public/icons'
import classNames from 'classnames/bind'
+import Textarea from '@/shared/ui/textarea'
+
+import useEditInformationStore from '../../my/strategies/manage/[strategyId]/_store/use-edit-information-store'
import styles from './styles.module.scss'
const cx = classNames.bind(styles)
interface Props {
content: string
+ isEditable?: boolean
}
-const StrategyIntroduction = ({ content }: Props) => {
+const StrategyIntroduction = ({ content, isEditable = false }: Props) => {
const [shouldShowMore, setShouldShowMore] = useState(false)
+ const [editContent, setEditContent] = useState(null)
const [isOverflow, setIsOverflow] = useState(false)
- const contentRef = useRef(null)
+ const contentRef = useRef(null)
+ const setDescription = useEditInformationStore((state) => state.actions.setDescription)
useEffect(() => {
checkOverflow()
+ setEditContent(content)
}, [content])
+ useEffect(() => {
+ if (editContent) {
+ setDescription(editContent as string)
+ }
+ }, [editContent])
+
const checkOverflow = () => {
if (contentRef.current) {
setIsOverflow(contentRef.current.scrollHeight > contentRef.current.offsetHeight)
@@ -32,7 +46,15 @@ const StrategyIntroduction = ({ content }: Props) => {
์ ๋ต ์์ธ ์๊ฐ
-
{content}
+ {isEditable ? (
+
{isOverflow && (
diff --git a/app/(dashboard)/_ui/introduction/styles.module.scss b/app/(dashboard)/_ui/introduction/styles.module.scss
index 773a3369..4a541c06 100644
--- a/app/(dashboard)/_ui/introduction/styles.module.scss
+++ b/app/(dashboard)/_ui/introduction/styles.module.scss
@@ -15,10 +15,13 @@
&.expand {
display: contents;
}
- p {
- @include typo-c1;
+ pre {
+ @include typo-b3;
line-height: 18px;
color: $color-gray-600;
+ word-wrap: break-word;
+ word-break: break-word;
+ white-space: pre-wrap;
}
}
@@ -34,7 +37,8 @@
color: $color-gray-500;
background-color: transparent;
svg {
- margin: -3px 0 0 7px;
+ margin-top: -3px;
+ width: 28px;
}
}
}
diff --git a/app/(dashboard)/_ui/list-header/index.tsx b/app/(dashboard)/_ui/list-header/index.tsx
index b8ace106..962fb45d 100644
--- a/app/(dashboard)/_ui/list-header/index.tsx
+++ b/app/(dashboard)/_ui/list-header/index.tsx
@@ -18,7 +18,13 @@ const ListHeader = ({ type = 'default' }: Props) => {
{LIST_HEADER[type].map((category) => (
- {category}
+ {category === 'SM SCORE' ? (
+ <>
+ SM SCORE
+ >
+ ) : (
+ category
+ )}
))}
diff --git a/app/(dashboard)/_ui/list-header/styles.module.scss b/app/(dashboard)/_ui/list-header/styles.module.scss
index 5efb4b05..27dbb8df 100644
--- a/app/(dashboard)/_ui/list-header/styles.module.scss
+++ b/app/(dashboard)/_ui/list-header/styles.module.scss
@@ -16,10 +16,19 @@
background-color: $color-white;
border: 1px solid $color-gray-200;
border-radius: 4px;
+ padding: 0 2px;
@include typo-b2;
-
+ @include tablet-md {
+ font-size: $text-b3;
+ }
&:not(:first-child) {
margin-left: 5px;
}
+ & span:nth-child(2) {
+ margin-left: 2px;
+ @include tablet-md {
+ display: none;
+ }
+ }
}
}
diff --git a/app/(dashboard)/_ui/navigation.tsx b/app/(dashboard)/_ui/navigation.tsx
index 951168a3..d5917965 100644
--- a/app/(dashboard)/_ui/navigation.tsx
+++ b/app/(dashboard)/_ui/navigation.tsx
@@ -10,7 +10,7 @@ import {
import { PATH } from '@/shared/constants/path'
import { useAuthStore } from '@/shared/stores/use-auth-store'
-import { isTrader } from '@/shared/types/auth'
+import { isSuperAdmin, isTrader } from '@/shared/types/auth'
import SideNavigation from '@/shared/ui/side-navigation'
import NavLinkItem from '@/shared/ui/side-navigation/nav-link-item'
@@ -25,7 +25,7 @@ const DashboardNavigation = () => {
ํธ๋ ์ด๋ ๋ชฉ๋ก
- {isTrader(user) && (
+ {(isTrader(user) || isSuperAdmin(user)) && (
๋์ ์ ๋ต
diff --git a/app/(dashboard)/_ui/strategies-item/area-chart.tsx b/app/(dashboard)/_ui/strategies-item/area-chart.tsx
index e03e012e..e3e1bfea 100644
--- a/app/(dashboard)/_ui/strategies-item/area-chart.tsx
+++ b/app/(dashboard)/_ui/strategies-item/area-chart.tsx
@@ -20,7 +20,9 @@ interface Props {
}
const AreaChart = ({ profitRateChartData: data }: Props) => {
- if (!data) return
+ if (!data?.dates || !data?.profitRates || data?.dates.length < 3 || data?.profitRates.length < 3)
+ return
์ค๋น์ค..
+
const chartOptions: Highcharts.Options = {
chart: {
type: 'areaspline',
diff --git a/app/(dashboard)/_ui/strategies-item/index.tsx b/app/(dashboard)/_ui/strategies-item/index.tsx
index 63a99ae4..2f4ee765 100644
--- a/app/(dashboard)/_ui/strategies-item/index.tsx
+++ b/app/(dashboard)/_ui/strategies-item/index.tsx
@@ -4,11 +4,10 @@ import classNames from 'classnames/bind'
import { PATH } from '@/shared/constants/path'
import useModal from '@/shared/hooks/custom/use-modal'
-import { useAuthStore } from '@/shared/stores/use-auth-store'
import { StrategiesModel } from '@/shared/types/strategy-data'
import { Button } from '@/shared/ui/button'
import { LinkButton } from '@/shared/ui/link-button'
-import SigninCheckModal from '@/shared/ui/modal/signin-check-modal'
+import StrategyDeleteModal from '@/shared/ui/modal/strategy-delete-modal'
import { formatNumber } from '@/shared/utils/format'
import AreaChart from './area-chart'
@@ -25,15 +24,14 @@ interface Props {
const StrategiesItem = ({ strategiesData: data, type = 'default' }: Props) => {
const router = useRouter()
- const user = useAuthStore((state) => state.user)
- const { isModalOpen, openModal, closeModal } = useModal()
+ const {
+ isModalOpen: isDeleteModalOpen,
+ openModal: openDeleteModal,
+ closeModal: closeDeleteModal,
+ } = useModal()
const handleRouter = () => {
- if (!user) {
- openModal()
- } else {
- router.push(`${PATH.STRATEGIES}/${data.strategyId}`)
- }
+ router.push(`${PATH.STRATEGIES}/${data.strategyId}`)
}
return (
@@ -56,7 +54,7 @@ const StrategiesItem = ({ strategiesData: data, type = 'default' }: Props) => {
{formatNumber(data.mdd)}
-
{data.smScore}
+
{data.smScore.toFixed(1)}
๋์ ์์ต๋ฅ
@@ -86,17 +84,30 @@ const StrategiesItem = ({ strategiesData: data, type = 'default' }: Props) => {
variant="filled"
href={`/my/strategies/manage/${data.strategyId}`}
className={cx('manage-button')}
+ onClick={(e) => e.stopPropagation()}
>
๊ด๋ฆฌ
-
+ {
+ e.stopPropagation()
+ openDeleteModal()
+ }}
+ >
์ญ์
>
)}
-
+
>
)
}
diff --git a/app/(dashboard)/_ui/strategies-item/strategies-summary.tsx b/app/(dashboard)/_ui/strategies-item/strategies-summary.tsx
index 78455cbb..d4c65e08 100644
--- a/app/(dashboard)/_ui/strategies-item/strategies-summary.tsx
+++ b/app/(dashboard)/_ui/strategies-item/strategies-summary.tsx
@@ -42,7 +42,7 @@ const StrategiesSummary = ({
๊ตฌ๋
{subscriptionCount}๊ฐ
-
|
+
|
diff --git a/app/(dashboard)/_ui/strategies-item/styles.module.scss b/app/(dashboard)/_ui/strategies-item/styles.module.scss
index 6f871b2d..06cd2296 100644
--- a/app/(dashboard)/_ui/strategies-item/styles.module.scss
+++ b/app/(dashboard)/_ui/strategies-item/styles.module.scss
@@ -31,6 +31,9 @@
.mdd,
.sm-score {
@include typo-b2;
+ @include tablet-md {
+ font-size: $text-b3;
+ }
}
.profit {
& span {
@@ -39,6 +42,9 @@
p {
@include typo-b2;
color: $color-orange-500;
+ @include tablet-md {
+ font-size: $text-b3;
+ }
}
}
}
@@ -61,6 +67,10 @@
& p {
margin-left: 10px;
@include typo-b2;
+ @include tablet-md {
+ font-size: $text-b3;
+ margin-left: 4px;
+ }
}
}
.total-subscribe-star {
@@ -69,9 +79,15 @@
height: 20px;
gap: 6px;
padding-top: 10px;
- & p {
+ & p,
+ span {
@include typo-c1;
}
+ @include tablet-md {
+ & span {
+ display: none;
+ }
+ }
}
}
@@ -80,6 +96,9 @@
background: transparent;
svg {
width: 36px;
+ @include tablet-md {
+ width: 30px;
+ }
}
}
}
@@ -93,6 +112,9 @@
display: flex;
align-items: center;
column-gap: 4px;
+ overflow: hidden;
+ min-height: 24px;
+ max-height: 44px;
.icon-wrapper {
.icon {
position: relative;
@@ -112,7 +134,11 @@
}
&.details {
flex-wrap: wrap;
- max-height: 50px;
row-gap: 1px;
}
}
+
+.no-data {
+ @include typo-b3;
+ color: $color-gray-600;
+}
diff --git a/app/(dashboard)/_ui/strategies-item/subscribe.tsx b/app/(dashboard)/_ui/strategies-item/subscribe.tsx
index 8e9d7a78..68a31bac 100644
--- a/app/(dashboard)/_ui/strategies-item/subscribe.tsx
+++ b/app/(dashboard)/_ui/strategies-item/subscribe.tsx
@@ -27,9 +27,7 @@ const Subscribe = ({ strategyId, subscriptionStatus, traderName }: Props) => {
const { mutate } = useGetSubscribe()
useEffect(() => {
- if (subscriptionStatus) {
- setIsSubscribed(true)
- }
+ setIsSubscribed(subscriptionStatus)
}, [subscriptionStatus])
const handleSubscribe = (e: React.MouseEvent) => {
diff --git a/app/(dashboard)/_ui/subscriber-item/index.tsx b/app/(dashboard)/_ui/subscriber-item/index.tsx
index 68c73bd0..ac2c319d 100644
--- a/app/(dashboard)/_ui/subscriber-item/index.tsx
+++ b/app/(dashboard)/_ui/subscriber-item/index.tsx
@@ -1,8 +1,12 @@
'use client'
+import { usePathname } from 'next/navigation'
+
import classNames from 'classnames/bind'
+import { PATH } from '@/shared/constants/path'
import { Button } from '@/shared/ui/button'
+import { LinkButton } from '@/shared/ui/link-button'
import styles from './styles.module.scss'
@@ -10,23 +14,45 @@ const cx = classNames.bind(styles)
interface Props {
isMyStrategy?: boolean
+ isEditable?: boolean
isSubscribed?: boolean
+ strategyId?: number
subscribers: number
onClick?: () => void
}
-const SubscriberItem = ({ isSubscribed, isMyStrategy = false, subscribers, onClick }: Props) => {
+const SubscriberItem = ({
+ isSubscribed,
+ isEditable = false,
+ isMyStrategy = false,
+ strategyId,
+ subscribers,
+ onClick,
+}: Props) => {
+ const currentPath = usePathname()
+
return (
-
+
๊ตฌ๋
|
{subscribers}
- {!isMyStrategy && (
+ {!isMyStrategy ? (
{isSubscribed ? '๊ตฌ๋
์ทจ์' : '๊ตฌ๋
ํ๊ธฐ'}
+ ) : (
+ !currentPath.includes(PATH.STRATEGIES_MANAGE) && (
+
+ ๊ด๋ฆฌํ๊ธฐ
+
+ )
)}
)
diff --git a/app/(dashboard)/_ui/subscriber-item/styles.module.scss b/app/(dashboard)/_ui/subscriber-item/styles.module.scss
index 4d49f1e8..2ec9d8de 100644
--- a/app/(dashboard)/_ui/subscriber-item/styles.module.scss
+++ b/app/(dashboard)/_ui/subscriber-item/styles.module.scss
@@ -7,10 +7,24 @@
border-radius: 5px;
background-color: $color-white;
margin-bottom: 18px;
+
+ &.edit {
+ background-color: $color-gray-200;
+ }
+
span {
font-size: 18px;
font-weight: $text-semibold;
color: $color-gray-800;
margin-left: 4px;
}
+ .trader-button {
+ padding: 10px 20px;
+ }
+ @include tablet-md {
+ padding: 0 20px;
+ span {
+ font-size: $text-b2;
+ }
+ }
}
diff --git a/app/(dashboard)/my/_api/add-strategy.ts b/app/(dashboard)/my/_api/add-strategy.ts
index d78a692d..fa33b7a4 100644
--- a/app/(dashboard)/my/_api/add-strategy.ts
+++ b/app/(dashboard)/my/_api/add-strategy.ts
@@ -49,14 +49,15 @@ export interface StrategyModel {
stockTypeIds: number[]
minimumInvestmentAmount: MinimumInvestmentAmountType
description: string
- proposalFile?: ProposalFileInfoModel
+ proposalFile: ProposalFileInfoModel | null
}
export interface StrategyResponseModel {
isSuccess: boolean
message: string
result: {
- presignedUrl: string
+ strategyId: number
+ presignedUrl?: string
}
code: number
}
diff --git a/app/(dashboard)/my/_api/delete-my-strategy.ts b/app/(dashboard)/my/_api/delete-my-strategy.ts
new file mode 100644
index 00000000..4bf3d10e
--- /dev/null
+++ b/app/(dashboard)/my/_api/delete-my-strategy.ts
@@ -0,0 +1,15 @@
+import axiosInstance from '@/shared/api/axios'
+import { APIResponseBaseModel } from '@/shared/types/response'
+
+export interface DeleteStrategyResponseModel extends APIResponseBaseModel
{
+ result: Record
+}
+
+export const deleteMyStrategy = async (
+ strategyId: number
+): Promise => {
+ const { data } = await axiosInstance.delete(
+ `/api/my-strategies/${strategyId}`
+ )
+ return data
+}
diff --git a/app/(dashboard)/my/_api/delete-user-withdrawal.ts b/app/(dashboard)/my/_api/delete-user-withdrawal.ts
new file mode 100644
index 00000000..31236cbb
--- /dev/null
+++ b/app/(dashboard)/my/_api/delete-user-withdrawal.ts
@@ -0,0 +1,13 @@
+import axiosInstance from '@/shared/api/axios'
+
+interface WithDrawUserResponseModel {
+ isSuccess: boolean
+ message: string
+ result: T
+ code: number
+}
+
+export const deleteUser = async () => {
+ const response = await axiosInstance.delete>('/api/users')
+ return response.data
+}
diff --git a/app/(dashboard)/my/_api/get-my-account-iamges.ts b/app/(dashboard)/my/_api/get-my-account-iamges.ts
index 297958f9..f9d3d88b 100644
--- a/app/(dashboard)/my/_api/get-my-account-iamges.ts
+++ b/app/(dashboard)/my/_api/get-my-account-iamges.ts
@@ -1,20 +1,9 @@
-import { ImageDataModel } from '@/app/(dashboard)/_ui/analysis-container/account-content'
-
import axiosInstance from '@/shared/api/axios'
-
-interface ResponseModel {
- content: ImageDataModel
- first: boolean
- last: boolean
- page: number
- size: number
- totalElements: number
- totalPages: number
-}
+import { AccountImageDataModel } from '@/shared/types/strategy-data'
const getMyAccountImages = async (
strategyId: number
-): Promise => {
+): Promise => {
try {
const response = await axiosInstance.get(`/api/my-strategies/${strategyId}/account-images`)
return response.data.result
diff --git a/app/(dashboard)/my/_api/get-profile.ts b/app/(dashboard)/my/_api/get-profile.ts
index 2112cd32..761eb4ed 100644
--- a/app/(dashboard)/my/_api/get-profile.ts
+++ b/app/(dashboard)/my/_api/get-profile.ts
@@ -10,6 +10,7 @@ export interface ProfileModel {
infoAgreement: boolean
role: string
birthDate: string
+ joinDate: string
}
interface ProfileResponseModel {
diff --git a/app/(dashboard)/my/_api/get-proposal-file-name.ts b/app/(dashboard)/my/_api/get-proposal-file-name.ts
new file mode 100644
index 00000000..32fd32bd
--- /dev/null
+++ b/app/(dashboard)/my/_api/get-proposal-file-name.ts
@@ -0,0 +1,34 @@
+import axiosInstance from '@/shared/api/axios'
+
+interface StrategyDetailsResponseModel {
+ isSuccess: boolean
+ message: string
+ result: {
+ strategyName: string
+ tradeType: {
+ tradeTypeId: number
+ tradeTypeName: string
+ tradeTypeIconUrl: string
+ }
+ stockTypes: Array<{
+ stockTypeId: number
+ stockTypeName: string
+ stockIconUrl: string
+ }>
+ minimumInvestmentAmount: string
+ operationCycle: string
+ proposalFileName: string
+ proposalFileUrl: string
+ isPublic: boolean
+ description: string
+ }
+}
+
+const getProposalFileName = async (strategyId: number) => {
+ const response = await axiosInstance.get(
+ `/api/my-strategies/modify/${strategyId}`
+ )
+ return response.data
+}
+
+export default getProposalFileName
diff --git a/app/(dashboard)/my/_api/manage-daily-analysis.ts b/app/(dashboard)/my/_api/manage-daily-analysis.ts
new file mode 100644
index 00000000..3c8206dc
--- /dev/null
+++ b/app/(dashboard)/my/_api/manage-daily-analysis.ts
@@ -0,0 +1,27 @@
+import axiosInstance from '@/shared/api/axios'
+
+export interface EditAnalysisPayloadModel {
+ date: string
+ transaction: number
+ dailyProfitLoss: number
+}
+
+export const editAnalysis = async (strategyId: number, payload: EditAnalysisPayloadModel) => {
+ const response = await axiosInstance.patch(
+ `/api/my-strategies/${strategyId}/daily-analysis`,
+ payload
+ )
+ return response.data
+}
+
+export const deleteAnalysis = async (strategyId: number, analysisId: number) => {
+ const response = await axiosInstance.delete(
+ `/api/my-strategies/${strategyId}/daily-analysis?analysisId=${analysisId}`
+ )
+ return response.data
+}
+
+export const deleteAllAnalysis = async (strategyId: number) => {
+ const response = await axiosInstance.delete(`/api/my-strategies/${strategyId}/daily-analysis/all`)
+ return response.data
+}
diff --git a/app/(dashboard)/my/_api/patch-profile.ts b/app/(dashboard)/my/_api/patch-user-profile.ts
similarity index 87%
rename from app/(dashboard)/my/_api/patch-profile.ts
rename to app/(dashboard)/my/_api/patch-user-profile.ts
index e613effb..953bfe7d 100644
--- a/app/(dashboard)/my/_api/patch-profile.ts
+++ b/app/(dashboard)/my/_api/patch-user-profile.ts
@@ -1,6 +1,6 @@
import axiosInstance from '@/shared/api/axios'
-import { UserProfileModel } from '../_hooks/query/use-patch-profile'
+import { UserProfileModel } from '../_hooks/query/use-patch-user-profile'
interface PatchUserProfileModel {
isSuccess: boolean
diff --git a/app/(dashboard)/my/_api/post-account-image.ts b/app/(dashboard)/my/_api/post-account-image.ts
index 7e433c37..a4a5d15c 100644
--- a/app/(dashboard)/my/_api/post-account-image.ts
+++ b/app/(dashboard)/my/_api/post-account-image.ts
@@ -44,6 +44,5 @@ export const deleteAccountImages = async ({
`/api/my-strategies/${strategyId}/delete-account-images`,
imageIds
)
- console.log(response.data)
return response.data
}
diff --git a/app/(dashboard)/my/_api/post-edit-strategy.ts b/app/(dashboard)/my/_api/post-edit-strategy.ts
new file mode 100644
index 00000000..e385ac18
--- /dev/null
+++ b/app/(dashboard)/my/_api/post-edit-strategy.ts
@@ -0,0 +1,39 @@
+import { AxiosError } from 'axios'
+
+import axiosInstance from '@/shared/api/axios'
+
+export interface ContentModel {
+ strategyName: string
+ description: string
+ proposalFile?: {
+ proposalFileName: string
+ proposalFileSize: number
+ }
+ proposalModified: boolean
+}
+
+export interface EditStrategyResponseModel {
+ isSuccess: boolean
+ message: string
+ result: {
+ presignedUrl?: string
+ }
+ code: number
+}
+
+const postEditStrategy = async (
+ strategyId: number,
+ information: ContentModel
+): Promise => {
+ try {
+ const response = await axiosInstance.post(
+ `/api/my-strategies/modify/${strategyId}`,
+ information
+ )
+ return response.data
+ } catch (err) {
+ throw new Error('์๋ฒ ์ค๋ฅ๋ก ์ ์์ ์์ ์ ์คํจํ์ต๋๋ค.', err as AxiosError)
+ }
+}
+
+export default postEditStrategy
diff --git a/app/(dashboard)/my/_hooks/query/use-add-strategy.ts b/app/(dashboard)/my/_hooks/query/use-add-strategy.ts
index 73f69c94..70320649 100644
--- a/app/(dashboard)/my/_hooks/query/use-add-strategy.ts
+++ b/app/(dashboard)/my/_hooks/query/use-add-strategy.ts
@@ -5,6 +5,9 @@ import { useRouter } from 'next/navigation'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { AxiosError } from 'axios'
+import uploadFileWithPresignedUrl from '@/shared/api/upload-file-with-presigned-url'
+import { QUERY_KEY } from '@/shared/constants/query-key'
+
import {
StrategyModel,
StrategyResponseModel,
@@ -20,26 +23,43 @@ export const useAddStrategy = () => {
const router = useRouter()
const queryClient = useQueryClient()
const [error, setError] = useState(null)
+ const [isUploading, setIsUploading] = useState(false)
const { data: strategyTypes, isLoading: isTypesLoading } = useQuery<
StrategyTypeResponseModel,
AxiosError
>({
- queryKey: ['strategyTypes'],
+ queryKey: [QUERY_KEY.STRATEGY_TYPES],
queryFn: () => strategyApi.getStrategyTypes().then((response) => response.data),
retry: false,
refetchOnWindowFocus: false,
})
- const mutation = useMutation<
+ const registerStrategyMutation = useMutation<
StrategyResponseModel,
AxiosError,
- StrategyModel
+ { data: StrategyModel; file?: File }
>({
- mutationFn: (data) => strategyApi.registerStrategy(data).then((response) => response.data),
+ mutationFn: async ({ data, file }) => {
+ const response = await strategyApi.registerStrategy(data)
+
+ if (file && response.data.result?.presignedUrl) {
+ setIsUploading(true)
+ try {
+ await uploadFileWithPresignedUrl(response.data.result.presignedUrl, file)
+ } catch (err) {
+ console.error('File upload failed:', err)
+ throw new Error('ํ์ผ ์
๋ก๋ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.')
+ } finally {
+ setIsUploading(false)
+ }
+ }
+
+ return response.data
+ },
onSuccess: () => {
queryClient.invalidateQueries({
- queryKey: ['addStrategies'],
+ queryKey: [QUERY_KEY.MY_STRATEGIES],
})
router.back()
},
@@ -49,11 +69,15 @@ export const useAddStrategy = () => {
},
})
+ const registerStrategy = async (data: StrategyModel, file?: File) => {
+ await registerStrategyMutation.mutateAsync({ data, file })
+ }
+
return {
strategyTypes: strategyTypes?.result,
isTypesLoading,
- registerStrategy: mutation.mutate,
- isRegistering: mutation.isPending,
+ registerStrategy,
+ isRegistering: registerStrategyMutation.isPending || isUploading,
error,
}
}
diff --git a/app/(dashboard)/my/_hooks/query/use-analysis-mutation.ts b/app/(dashboard)/my/_hooks/query/use-analysis-mutation.ts
index fae29d08..79713780 100644
--- a/app/(dashboard)/my/_hooks/query/use-analysis-mutation.ts
+++ b/app/(dashboard)/my/_hooks/query/use-analysis-mutation.ts
@@ -5,6 +5,7 @@ import {
} from '@/app/(dashboard)/my/_api/post-daily-analysis'
import { useMutation, useQueryClient } from '@tanstack/react-query'
+import { QUERY_KEY } from '@/shared/constants/query-key'
import { AnalysisDataModel } from '@/shared/types/strategy-data'
interface UploadMutationParamsModel {
@@ -22,14 +23,14 @@ export const useAnalysisUploadMutation = (
mutationFn: ({ data }) => uploadDailyAnalysis(strategyId, data),
onSuccess: async () => {
queryClient.invalidateQueries({
- queryKey: ['myDailyAnalysis', strategyId],
+ queryKey: [QUERY_KEY.MY_DAILY_ANALYSIS, strategyId],
})
try {
const newData = await getMyDailyAnalysis(strategyId, page, size)
- queryClient.setQueryData(['myDailyAnalysis', strategyId], newData)
- } catch (error) {
- console.error('Failed to fetch updated my daily analysis data:', error)
+ queryClient.setQueryData([QUERY_KEY.MY_DAILY_ANALYSIS, strategyId], newData)
+ } catch (err) {
+ console.error('Failed to fetch updated my daily analysis data:', err)
}
},
})
@@ -38,14 +39,14 @@ export const useAnalysisUploadMutation = (
mutationFn: () => deleteAllAnalysis(strategyId),
onSuccess: async () => {
queryClient.invalidateQueries({
- queryKey: ['myDailyAnalysis', strategyId],
+ queryKey: [QUERY_KEY.MY_DAILY_ANALYSIS, strategyId],
})
try {
const newData = await getMyDailyAnalysis(strategyId, page, size)
- queryClient.setQueryData(['myDailyAnalysis', strategyId], newData)
- } catch (error) {
- console.error('Failed to fetch updated my daily analysis data:', error)
+ queryClient.setQueryData([QUERY_KEY.MY_DAILY_ANALYSIS, strategyId], newData)
+ } catch (err) {
+ console.error('Failed to fetch updated my daily analysis data:', err)
}
},
})
diff --git a/app/(dashboard)/my/_hooks/query/use-delete-account-images.ts b/app/(dashboard)/my/_hooks/query/use-delete-account-images.ts
index bb376905..4400262b 100644
--- a/app/(dashboard)/my/_hooks/query/use-delete-account-images.ts
+++ b/app/(dashboard)/my/_hooks/query/use-delete-account-images.ts
@@ -1,5 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
+import { QUERY_KEY } from '@/shared/constants/query-key'
+
import { deleteAccountImages } from '../../_api/post-account-image'
interface DeleteAccountImagesRequestModel {
@@ -14,7 +16,7 @@ export const useDeleteAccountImages = () => {
mutationFn: (request: DeleteAccountImagesRequestModel) => deleteAccountImages(request),
onSuccess: (_, request) => {
queryClient.invalidateQueries({
- queryKey: ['myAccountImages', request.strategyId],
+ queryKey: [QUERY_KEY.MY_ACCOUNT_IMAGES, request.strategyId],
})
},
})
diff --git a/app/(dashboard)/my/_hooks/query/use-delete-my-strategy.ts b/app/(dashboard)/my/_hooks/query/use-delete-my-strategy.ts
new file mode 100644
index 00000000..38975959
--- /dev/null
+++ b/app/(dashboard)/my/_hooks/query/use-delete-my-strategy.ts
@@ -0,0 +1,20 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query'
+
+import { QUERY_KEY } from '@/shared/constants/query-key'
+
+import { deleteMyStrategy } from '../../_api/delete-my-strategy'
+
+export const useDeleteMyStrategy = () => {
+ const queryClient = useQueryClient()
+
+ const { mutate, isPending } = useMutation({
+ mutationFn: (strategyId: number) => deleteMyStrategy(strategyId),
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: [QUERY_KEY.MY_STRATEGIES],
+ })
+ },
+ })
+
+ return { mutate, isPending }
+}
diff --git a/app/(dashboard)/my/_hooks/query/use-get-favorite-strategy-list.ts b/app/(dashboard)/my/_hooks/query/use-get-favorite-strategy-list.ts
index 7138a936..85069e01 100644
--- a/app/(dashboard)/my/_hooks/query/use-get-favorite-strategy-list.ts
+++ b/app/(dashboard)/my/_hooks/query/use-get-favorite-strategy-list.ts
@@ -1,5 +1,7 @@
import { useQuery } from '@tanstack/react-query'
+import { QUERY_KEY } from '@/shared/constants/query-key'
+
import getFavoriteStrategyList from '../../_api/get-favorite-strategy-list'
interface Props {
@@ -9,7 +11,7 @@ interface Props {
const useGetFavoriteStrategyList = ({ page, size }: Props) => {
return useQuery({
- queryKey: ['favoriteStrategies', page, size],
+ queryKey: [QUERY_KEY.MY_FAVORITE_STRATEGIES, page, size],
queryFn: () => getFavoriteStrategyList({ page, size }),
})
}
diff --git a/app/(dashboard)/my/_hooks/query/use-get-my-account-image.ts b/app/(dashboard)/my/_hooks/query/use-get-my-account-image.ts
index 7b9d32bc..3ff17411 100644
--- a/app/(dashboard)/my/_hooks/query/use-get-my-account-image.ts
+++ b/app/(dashboard)/my/_hooks/query/use-get-my-account-image.ts
@@ -1,10 +1,12 @@
import { useQuery } from '@tanstack/react-query'
+import { QUERY_KEY } from '@/shared/constants/query-key'
+
import getMyAccountImages from '../../_api/get-my-account-iamges'
const useGetMyAccountImages = (strategyId: number) => {
return useQuery({
- queryKey: ['myAccountImages', strategyId],
+ queryKey: [QUERY_KEY.MY_ACCOUNT_IMAGES, strategyId],
queryFn: () => getMyAccountImages(strategyId),
})
}
diff --git a/app/(dashboard)/my/_hooks/query/use-get-my-daily-analysis.ts b/app/(dashboard)/my/_hooks/query/use-get-my-daily-analysis.ts
index 3df8e5c8..7908e152 100644
--- a/app/(dashboard)/my/_hooks/query/use-get-my-daily-analysis.ts
+++ b/app/(dashboard)/my/_hooks/query/use-get-my-daily-analysis.ts
@@ -1,12 +1,14 @@
import { useQuery } from '@tanstack/react-query'
+import { QUERY_KEY } from '@/shared/constants/query-key'
+
import getMyDailyAnalysis from '../../_api/get-my-daily-analysis'
-const useGetAnalysis = (strategyId: number, page: number, size: number) => {
+const useGetMyDailyAnalysis = (strategyId: number, page: number, size: number) => {
return useQuery({
- queryKey: ['myDailyAnalysis', strategyId, page],
+ queryKey: [QUERY_KEY.MY_DAILY_ANALYSIS, strategyId, page, size],
queryFn: () => getMyDailyAnalysis(strategyId, page, size),
})
}
-export default useGetAnalysis
+export default useGetMyDailyAnalysis
diff --git a/app/(dashboard)/my/_hooks/query/use-get-my-strategy-list.ts b/app/(dashboard)/my/_hooks/query/use-get-my-strategy-list.ts
index f0f096e2..e26c3e4d 100644
--- a/app/(dashboard)/my/_hooks/query/use-get-my-strategy-list.ts
+++ b/app/(dashboard)/my/_hooks/query/use-get-my-strategy-list.ts
@@ -1,6 +1,7 @@
import { getMyStrategyList } from '@/app/(dashboard)/my/_api/get-my-strategy-list'
import { useInfiniteQuery } from '@tanstack/react-query'
+import { QUERY_KEY } from '@/shared/constants/query-key'
import { StrategiesModel } from '@/shared/types/strategy-data'
interface StrategiesPageModel {
@@ -10,7 +11,7 @@ interface StrategiesPageModel {
export const useGetMyStrategyList = () => {
return useInfiniteQuery({
- queryKey: ['myStrategies'],
+ queryKey: [QUERY_KEY.MY_STRATEGIES],
queryFn: async ({ pageParam = 1 }) => {
const page = typeof pageParam === 'number' ? pageParam : 1
return getMyStrategyList({ page, size: 4 })
diff --git a/app/(dashboard)/my/_hooks/query/use-get-profile.ts b/app/(dashboard)/my/_hooks/query/use-get-profile.ts
index 67ee9ebc..63ea4333 100644
--- a/app/(dashboard)/my/_hooks/query/use-get-profile.ts
+++ b/app/(dashboard)/my/_hooks/query/use-get-profile.ts
@@ -1,10 +1,12 @@
import { useQuery } from '@tanstack/react-query'
+import { QUERY_KEY } from '@/shared/constants/query-key'
+
import { getProfile } from './../../_api/get-profile'
const useGetProfile = () => {
return useQuery({
- queryKey: ['userProfile'],
+ queryKey: [QUERY_KEY.MY_PROFILE],
queryFn: getProfile,
})
}
diff --git a/app/(dashboard)/my/_hooks/query/use-get-proposal-file-name.ts b/app/(dashboard)/my/_hooks/query/use-get-proposal-file-name.ts
new file mode 100644
index 00000000..2764fb87
--- /dev/null
+++ b/app/(dashboard)/my/_hooks/query/use-get-proposal-file-name.ts
@@ -0,0 +1,15 @@
+import { useQuery } from '@tanstack/react-query'
+
+import { QUERY_KEY } from '@/shared/constants/query-key'
+
+import getProposalFileName from '../../_api/get-proposal-file-name'
+
+const useGetProposalFileName = (strategyId: number) => {
+ return useQuery({
+ queryKey: [QUERY_KEY.STRATEGY_PROPOSAL_FILE_NAME, strategyId],
+ queryFn: () => getProposalFileName(strategyId),
+ enabled: !!strategyId,
+ })
+}
+
+export default useGetProposalFileName
diff --git a/app/(dashboard)/my/_hooks/query/use-manage-daily-analysis.ts b/app/(dashboard)/my/_hooks/query/use-manage-daily-analysis.ts
new file mode 100644
index 00000000..126e9372
--- /dev/null
+++ b/app/(dashboard)/my/_hooks/query/use-manage-daily-analysis.ts
@@ -0,0 +1,69 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query'
+
+import { QUERY_KEY } from '@/shared/constants/query-key'
+
+import {
+ EditAnalysisPayloadModel,
+ deleteAllAnalysis,
+ deleteAnalysis,
+ editAnalysis,
+} from '../../_api/manage-daily-analysis'
+
+export const useMyAnalysisMutation = (strategyId: number, page: number, size: number) => {
+ const queryClient = useQueryClient()
+ const queryKey = [QUERY_KEY.MY_DAILY_ANALYSIS, strategyId, page, size]
+
+ const { mutate: editAnalysisData } = useMutation({
+ mutationFn: ({ payload }: { payload: EditAnalysisPayloadModel }) => {
+ if (!strategyId) {
+ throw new Error('Strategy ID is required')
+ }
+ return editAnalysis(strategyId, payload)
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey })
+ },
+ onError: (err: Error) => {
+ console.error('Edit error:', err)
+ throw err
+ },
+ })
+
+ const { mutate: deleteAnalysisData } = useMutation({
+ mutationFn: (analysisId: number) => {
+ if (!strategyId) {
+ throw new Error('Strategy ID is required')
+ }
+ return deleteAnalysis(strategyId, analysisId)
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey })
+ },
+ onError: (err: Error) => {
+ console.error('Delete error:', err)
+ throw err
+ },
+ })
+
+ const { mutate: deleteAllAnalysisData } = useMutation({
+ mutationFn: () => {
+ if (!strategyId) {
+ throw new Error('Strategy ID is required')
+ }
+ return deleteAllAnalysis(strategyId)
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey })
+ },
+ onError: (err: Error) => {
+ console.error('Delete all error:', err)
+ throw err
+ },
+ })
+
+ return {
+ editAnalysisData,
+ deleteAnalysisData,
+ deleteAllAnalysisData,
+ }
+}
diff --git a/app/(dashboard)/my/_hooks/query/use-patch-profile.ts b/app/(dashboard)/my/_hooks/query/use-patch-profile.ts
deleted file mode 100644
index 6d122247..00000000
--- a/app/(dashboard)/my/_hooks/query/use-patch-profile.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { useMutation, useQueryClient } from '@tanstack/react-query'
-
-import patchProfile from '../../_api/patch-profile'
-
-export interface UserProfileModel {
- nickname?: string
- password?: string
- imageDto?: {
- imageName: string
- size: number
- }
- phone?: string
- email?: string
- imageChange: boolean
-}
-
-const usePatchUserProfile = () => {
- const queryClient = useQueryClient()
-
- return useMutation({
- mutationFn: (data: UserProfileModel) => patchProfile(data),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['userProfile'] })
- },
- onError: (error) => {
- console.error('Error updating user profile:', error)
- },
- })
-}
-
-export default usePatchUserProfile
diff --git a/app/(dashboard)/my/_hooks/query/use-patch-user-profile.ts b/app/(dashboard)/my/_hooks/query/use-patch-user-profile.ts
new file mode 100644
index 00000000..ab68328c
--- /dev/null
+++ b/app/(dashboard)/my/_hooks/query/use-patch-user-profile.ts
@@ -0,0 +1,93 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query'
+
+import { QUERY_KEY } from '@/shared/constants/query-key'
+
+import patchProfile from '../../_api/patch-user-profile'
+
+export interface UserProfileModel {
+ nickname?: string | null
+ password?: string | null
+ imageDto?: {
+ imageName: string
+ size: number
+ } | null
+ phone?: string | null
+ email?: string | null
+ imageChange: boolean
+}
+
+interface UpdateProfileModel {
+ profileData: UserProfileModel
+ imageFile?: File | null
+}
+
+interface PatchProfileResponseModel {
+ isSuccess: boolean
+ message: string
+ result: string
+ code: number
+}
+interface UserProfileDataModel {
+ nickname: string
+ imageUrl: string | null
+ phone: string | null
+ email: string
+}
+
+const usePatchUserProfile = () => {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: async ({ profileData, imageFile }: UpdateProfileModel) => {
+ try {
+ const updateData = {
+ ...profileData,
+ ...(imageFile && {
+ imageDto: {
+ imageName: imageFile.name,
+ size: imageFile.size,
+ },
+ }),
+ }
+
+ const profileResponse = await patchProfile(updateData)
+
+ if (imageFile && profileResponse.result) {
+ await fetch(profileResponse.result, {
+ method: 'PUT',
+ body: imageFile,
+ headers: {
+ 'Content-Type': imageFile.type,
+ },
+ })
+ }
+
+ return profileResponse
+ } catch (err) {
+ if (err instanceof Error) {
+ throw new Error(`ํ๋กํ ์
๋ฐ์ดํธ ์คํจ: ${err.message}`)
+ }
+ throw err
+ }
+ },
+ onSuccess: (data) => {
+ queryClient.invalidateQueries({ queryKey: [QUERY_KEY.MY_PROFILE] })
+
+ if (data.result) {
+ const imageUrl = data.result.split('?')[0]
+ queryClient.setQueryData([QUERY_KEY.MY_PROFILE], (oldData) => {
+ if (!oldData) return oldData
+ return {
+ ...oldData,
+ imageUrl,
+ }
+ })
+ }
+ },
+ onError: (err: Error) => {
+ console.error('ํ๋กํ ์
๋ฐ์ดํธ ์คํจ:', err.message)
+ },
+ })
+}
+
+export default usePatchUserProfile
diff --git a/app/(dashboard)/my/_hooks/query/use-post-edit-strategy.ts b/app/(dashboard)/my/_hooks/query/use-post-edit-strategy.ts
new file mode 100644
index 00000000..5b26782e
--- /dev/null
+++ b/app/(dashboard)/my/_hooks/query/use-post-edit-strategy.ts
@@ -0,0 +1,33 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query'
+
+import uploadFileWithPresignedUrl from '@/shared/api/upload-file-with-presigned-url'
+import { QUERY_KEY } from '@/shared/constants/query-key'
+
+import postEditStrategy, {
+ ContentModel,
+ EditStrategyResponseModel,
+} from '../../_api/post-edit-strategy'
+
+const usePostEditStrategy = () => {
+ const queryClient = useQueryClient()
+
+ return useMutation<
+ EditStrategyResponseModel,
+ unknown,
+ { strategyId: number; information: ContentModel; file?: File }
+ >({
+ mutationFn: async ({ strategyId, information, file }) => {
+ const response = await postEditStrategy(strategyId, information)
+
+ if (file && information.proposalModified && response.result.presignedUrl) {
+ await uploadFileWithPresignedUrl(response.result.presignedUrl, file)
+ }
+ return response
+ },
+ onSuccess: (_, { strategyId }) => {
+ queryClient.invalidateQueries({ queryKey: [QUERY_KEY.STRATEGY_DETAILS, strategyId] })
+ },
+ })
+}
+
+export default usePostEditStrategy
diff --git a/app/(dashboard)/my/_hooks/query/use-upload-account-images.ts b/app/(dashboard)/my/_hooks/query/use-upload-account-images.ts
index aa7d3a76..c190fe0a 100644
--- a/app/(dashboard)/my/_hooks/query/use-upload-account-images.ts
+++ b/app/(dashboard)/my/_hooks/query/use-upload-account-images.ts
@@ -1,5 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
+import { QUERY_KEY } from '@/shared/constants/query-key'
+
import { uploadAccountImages } from '../../_api/post-account-image'
interface UseUploadAccountImagesProps {
@@ -40,7 +42,7 @@ export const useUploadAccountImages = ({
},
onSuccess: () => {
queryClient.invalidateQueries({
- queryKey: ['myAccountImages', strategyId],
+ queryKey: [QUERY_KEY.MY_ACCOUNT_IMAGES, strategyId],
exact: true,
})
diff --git a/app/(dashboard)/my/_hooks/query/use-user-withdrawal.ts b/app/(dashboard)/my/_hooks/query/use-user-withdrawal.ts
new file mode 100644
index 00000000..94670ab6
--- /dev/null
+++ b/app/(dashboard)/my/_hooks/query/use-user-withdrawal.ts
@@ -0,0 +1,23 @@
+import { useRouter } from 'next/navigation'
+
+import { useMutation } from '@tanstack/react-query'
+
+import { PATH } from '@/shared/constants/path'
+import { useAuth } from '@/shared/hooks/custom/use-auth'
+
+import { deleteUser } from '../../_api/delete-user-withdrawal'
+
+export const useWithdraw = () => {
+ const router = useRouter()
+ const { logout } = useAuth()
+
+ return useMutation({
+ mutationFn: deleteUser,
+ onSuccess: (data) => {
+ if (data.isSuccess) {
+ logout()
+ router.replace(PATH.HOME)
+ }
+ },
+ })
+}
diff --git a/app/(dashboard)/my/profile/_hooks/custom/use-profile-form.ts b/app/(dashboard)/my/profile/_hooks/custom/use-profile-form.ts
new file mode 100644
index 00000000..2ef13bc1
--- /dev/null
+++ b/app/(dashboard)/my/profile/_hooks/custom/use-profile-form.ts
@@ -0,0 +1,247 @@
+import { ChangeEvent, useState } from 'react'
+
+import { SIGNUP_ERROR_MESSAGES } from '@/app/(landing)/signup/_constants/signup'
+
+import { checkNicknameDuplicate, checkPhoneDuplicate } from '@/shared/api/check-duplicate'
+import { isValidNickname, isValidPassword, isValidPhone } from '@/shared/utils/validation'
+
+import { ProfileModel } from '../../../_api/get-profile'
+import {
+ ProfileFormErrorsModel,
+ ProfileFormModel,
+ ProfileFormStateModel,
+} from '../../_ui/user-info/types'
+
+const initialFormState = {
+ isNicknameVerified: false,
+ isPhoneVerified: false,
+ isPasswordVerified: false,
+}
+
+export const useProfileForm = (profile: ProfileModel) => {
+ const initialForm: ProfileFormModel = {
+ name: profile?.userName || '',
+ nickname: profile?.nickname || '',
+ email: profile?.email || '',
+ password: '',
+ passwordConfirm: '',
+ phone: profile?.phone || '',
+ birthDate: profile?.birthDate || '',
+ joinDate: profile?.joinDate || '',
+ }
+
+ const [form, setForm] = useState(initialForm)
+ const [formState, setFormState] = useState(initialFormState)
+ const [errors, setErrors] = useState({})
+ const [hasNicknameChanged, setHasNicknameChanged] = useState(false)
+ const [hasPhoneChanged, setHasPhoneChanged] = useState(false)
+ const [successMessages, setSuccessMessages] = useState<{ [key: string]: string | null }>({
+ nickname: null,
+ phone: null,
+ password: null,
+ })
+
+ const handleInputChange = (e: ChangeEvent) => {
+ const { name, value } = e.target
+ setForm((prev) => ({ ...prev, [name]: value }))
+
+ if (name === 'nickname') {
+ const isChanged = value !== profile?.nickname
+ setHasNicknameChanged(isChanged)
+ if (isChanged) {
+ setFormState((prev) => ({ ...prev, isNicknameVerified: false }))
+ setSuccessMessages((prev) => ({ ...prev, nickname: null }))
+ if (value && !isValidNickname(value)) {
+ setErrors((prev) => ({ ...prev, nickname: SIGNUP_ERROR_MESSAGES.NICKNAME_LENGTH }))
+ } else {
+ setErrors((prev) => ({ ...prev, nickname: null }))
+ }
+ } else {
+ setFormState((prev) => ({ ...prev, isNicknameVerified: true }))
+ setErrors((prev) => ({ ...prev, nickname: null }))
+ }
+ }
+
+ if (name === 'phone') {
+ const isChanged = value !== profile?.phone
+ setHasPhoneChanged(isChanged)
+ if (isChanged) {
+ setFormState((prev) => ({ ...prev, isPhoneVerified: false }))
+ setSuccessMessages((prev) => ({ ...prev, phone: null }))
+ if (value && !isValidPhone(value)) {
+ setErrors((prev) => ({ ...prev, phone: SIGNUP_ERROR_MESSAGES.PHONE_INVALID }))
+ } else {
+ setErrors((prev) => ({ ...prev, phone: null }))
+ }
+ } else {
+ setFormState((prev) => ({ ...prev, isPhoneVerified: true }))
+ setErrors((prev) => ({ ...prev, phone: null }))
+ }
+ }
+
+ if (name === 'password' || name === 'passwordConfirm') {
+ setFormState((prev) => ({ ...prev, isPasswordVerified: false }))
+ setSuccessMessages((prev) => ({ ...prev, password: null }))
+ setErrors((prev) => ({ ...prev, password: null }))
+ }
+ }
+
+ const handleNicknameCheck = async () => {
+ if (!isValidNickname(form.nickname)) {
+ setErrors((prev) => ({ ...prev, nickname: SIGNUP_ERROR_MESSAGES.NICKNAME_LENGTH }))
+ return
+ }
+
+ try {
+ const response = await checkNicknameDuplicate(form.nickname)
+ if (response.result.isAvailable) {
+ setFormState((prev) => ({ ...prev, isNicknameVerified: true }))
+ setErrors((prev) => ({ ...prev, nickname: null }))
+ setSuccessMessages((prev) => ({ ...prev, nickname: '์ฌ์ฉํ ์ ์๋ ๋๋ค์์
๋๋ค.' }))
+ } else {
+ setFormState((prev) => ({ ...prev, isNicknameVerified: false }))
+ setErrors((prev) => ({ ...prev, nickname: SIGNUP_ERROR_MESSAGES.NICKNAME_DUPLICATED }))
+ setSuccessMessages((prev) => ({ ...prev, nickname: null }))
+ }
+ } catch (err) {
+ console.error('๋๋ค์ ์ค๋ณต ํ์ธ ์คํจ:', err)
+ setFormState((prev) => ({ ...prev, isNicknameVerified: false }))
+ setErrors((prev) => ({ ...prev, nickname: SIGNUP_ERROR_MESSAGES.NICKNAME_NOT_ALLOWED }))
+ setSuccessMessages((prev) => ({ ...prev, nickname: null }))
+ }
+ }
+
+ const handlePhoneCheck = async () => {
+ if (!isValidPhone(form.phone)) {
+ setErrors((prev) => ({ ...prev, phone: SIGNUP_ERROR_MESSAGES.PHONE_INVALID }))
+ return
+ }
+
+ try {
+ const response = await checkPhoneDuplicate(form.phone)
+ if (response.result.isAvailable) {
+ setFormState((prev) => ({ ...prev, isPhoneVerified: true }))
+ setErrors((prev) => ({ ...prev, phone: null }))
+ setSuccessMessages((prev) => ({ ...prev, phone: '์ฌ์ฉํ ์ ์๋ ํด๋ํฐ ๋ฒํธ์
๋๋ค.' }))
+ } else {
+ setFormState((prev) => ({ ...prev, isPhoneVerified: false }))
+ setErrors((prev) => ({ ...prev, phone: SIGNUP_ERROR_MESSAGES.PHONE_DUPLICATED }))
+ setSuccessMessages((prev) => ({ ...prev, phone: null }))
+ }
+ } catch (err) {
+ console.error('ํด๋ํฐ ๋ฒํธ ์ค๋ณต ํ์ธ ์คํจ:', err)
+ setFormState((prev) => ({ ...prev, isPhoneVerified: false }))
+ setErrors((prev) => ({ ...prev, phone: SIGNUP_ERROR_MESSAGES.PHONE_CHECK_FAILED }))
+ setSuccessMessages((prev) => ({ ...prev, phone: null }))
+ }
+ }
+
+ const handlePasswordCheck = () => {
+ if (!form.password && !form.passwordConfirm) {
+ setFormState((prev) => ({ ...prev, isPasswordVerified: true }))
+ setErrors((prev) => ({ ...prev, password: null }))
+ return
+ }
+
+ if (!form.password || !form.passwordConfirm) {
+ setErrors((prev) => ({
+ ...prev,
+ password: SIGNUP_ERROR_MESSAGES.PASSWORD_REQUIRED,
+ }))
+ setSuccessMessages((prev) => ({ ...prev, password: null }))
+ return
+ }
+
+ if (!isValidPassword(form.password)) {
+ setErrors((prev) => ({
+ ...prev,
+ password: SIGNUP_ERROR_MESSAGES.PASSWORD_INVALID,
+ }))
+ setSuccessMessages((prev) => ({ ...prev, password: null }))
+ return
+ }
+
+ if (form.password !== form.passwordConfirm) {
+ setErrors((prev) => ({
+ ...prev,
+ password: SIGNUP_ERROR_MESSAGES.PASSWORD_MISMATCH,
+ }))
+ setSuccessMessages((prev) => ({ ...prev, password: null }))
+ return
+ }
+
+ setFormState((prev) => ({ ...prev, isPasswordVerified: true }))
+ setErrors((prev) => ({ ...prev, password: null }))
+ setSuccessMessages((prev) => ({ ...prev, password: '๋น๋ฐ๋ฒํธ๊ฐ ํ์ธ๋์์ต๋๋ค.' }))
+ }
+
+ const validateChangedFields = () => {
+ const errors: ProfileFormErrorsModel = {}
+
+ if (hasNicknameChanged) {
+ if (!formState.isNicknameVerified) {
+ errors.nickname = SIGNUP_ERROR_MESSAGES.NICKNAME_CHECK_REQUIRED
+ }
+ }
+
+ if (hasPhoneChanged) {
+ if (!formState.isPhoneVerified) {
+ errors.phone = SIGNUP_ERROR_MESSAGES.PHONE_CHECK_REQUIRED
+ }
+ }
+
+ if (form.password || form.passwordConfirm) {
+ if (!formState.isPasswordVerified) {
+ errors.password = SIGNUP_ERROR_MESSAGES.PASSWORD_REQUIRED
+ }
+ }
+
+ setErrors(errors)
+ return Object.keys(errors).length === 0
+ }
+
+ const isFormValid = (isImageVerified: boolean) => {
+ if (!hasNicknameChanged && !hasPhoneChanged && !form.password && !form.passwordConfirm) {
+ return isImageVerified
+ }
+
+ if (hasNicknameChanged && !formState.isNicknameVerified) {
+ return false
+ }
+
+ if (hasPhoneChanged && !formState.isPhoneVerified) {
+ return false
+ }
+
+ if ((form.password || form.passwordConfirm) && !formState.isPasswordVerified) {
+ return false
+ }
+
+ return true
+ }
+
+ const getUpdatedFields = () => {
+ return {
+ nickname: hasNicknameChanged ? form.nickname : null,
+ phone: hasPhoneChanged ? form.phone : null,
+ password: form.password || null,
+ email: form.email,
+ }
+ }
+
+ return {
+ form,
+ formState,
+ errors,
+ successMessages,
+ hasNicknameChanged,
+ hasPhoneChanged,
+ handleInputChange,
+ handleNicknameCheck,
+ handlePhoneCheck,
+ handlePasswordCheck,
+ validateChangedFields,
+ isFormValid,
+ getUpdatedFields,
+ }
+}
diff --git a/app/(dashboard)/my/profile/_hooks/custom/use-profile-image.ts b/app/(dashboard)/my/profile/_hooks/custom/use-profile-image.ts
new file mode 100644
index 00000000..22fa90ef
--- /dev/null
+++ b/app/(dashboard)/my/profile/_hooks/custom/use-profile-image.ts
@@ -0,0 +1,74 @@
+import { ChangeEvent, useEffect, useState } from 'react'
+
+const MAX_IMAGE_SIZE = 2 * 1024 * 1024
+
+export const useProfileImage = () => {
+ const [selectedImage, setSelectedImage] = useState(null)
+ const [previewUrl, setPreviewUrl] = useState(null)
+ const [imageError, setImageError] = useState(null)
+ const [isImageVerified, setIsImageVerified] = useState(false)
+
+ useEffect(() => {
+ return () => {
+ if (previewUrl) {
+ URL.revokeObjectURL(previewUrl)
+ }
+ }
+ }, [previewUrl])
+
+ const validateImage = (file: File): boolean => {
+ setImageError(null)
+ setIsImageVerified(false)
+
+ if (!file.type.startsWith('image/')) {
+ setImageError('์ด๋ฏธ์ง ํ์ผ๋ง ์
๋ก๋ ๊ฐ๋ฅํฉ๋๋ค.')
+ return false
+ }
+
+ if (file.size > MAX_IMAGE_SIZE) {
+ setImageError('์ด๋ฏธ์ง ํฌ๊ธฐ๋ 2MB ์ดํ์ฌ์ผ ํฉ๋๋ค.')
+ return false
+ }
+
+ setIsImageVerified(true)
+ return true
+ }
+
+ const handleImageChange = (e: ChangeEvent) => {
+ const file = e.target.files?.[0]
+ if (!file) return
+
+ if (validateImage(file)) {
+ const objectUrl = URL.createObjectURL(file)
+ setPreviewUrl(objectUrl)
+ setSelectedImage(file)
+ } else {
+ setSelectedImage(null)
+ setPreviewUrl(null)
+ }
+ }
+
+ const handleImageDelete = () => {
+ if (previewUrl) {
+ URL.revokeObjectURL(previewUrl)
+ }
+ setSelectedImage(null)
+ setPreviewUrl(null)
+ setImageError(null)
+ setIsImageVerified(false)
+
+ const fileInput = document.getElementById('profile-image') as HTMLInputElement
+ if (fileInput) {
+ fileInput.value = ''
+ }
+ }
+
+ return {
+ selectedImage,
+ previewUrl,
+ imageError,
+ isImageVerified,
+ handleImageChange,
+ handleImageDelete,
+ }
+}
diff --git a/app/(dashboard)/my/profile/_ui/user-info/index.tsx b/app/(dashboard)/my/profile/_ui/user-info/index.tsx
index 0d3aa53d..86d9ad3b 100644
--- a/app/(dashboard)/my/profile/_ui/user-info/index.tsx
+++ b/app/(dashboard)/my/profile/_ui/user-info/index.tsx
@@ -1,262 +1,137 @@
'use client'
-import { ChangeEvent, useState } from 'react'
-
+import Image from 'next/image'
import { useRouter } from 'next/navigation'
-import { SIGNUP_ERROR_MESSAGES } from '@/app/(landing)/signup/_constants/signup'
import { CameraIcon } from '@/public/icons'
-import axios from 'axios'
import classNames from 'classnames/bind'
-import axiosInstance from '@/shared/api/axios'
-import { checkNicknameDuplicate, checkPhoneDuplicate } from '@/shared/api/check-duplicate'
import { PATH } from '@/shared/constants/path'
import Avatar from '@/shared/ui/avatar'
import { Button } from '@/shared/ui/button'
-import { Input } from '@/shared/ui/input'
+import Input from '@/shared/ui/input'
import { LinkButton } from '@/shared/ui/link-button'
import { ProfileModel } from '../../../_api/get-profile'
+import usePatchUserProfile from '../../../_hooks/query/use-patch-user-profile'
+import { useProfileForm } from '../../_hooks/custom/use-profile-form'
+import { useProfileImage } from '../../_hooks/custom/use-profile-image'
import styles from './styles.module.scss'
-import { ProfileFormErrorsModel, ProfileFormModel, ProfileFormStateModel } from './types'
-import { validateProfileForm } from './utils'
const cx = classNames.bind(styles)
-const initialFormState = {
- isNicknameVerified: false,
- isPhoneVerified: false,
-}
-
interface Props {
isEditable?: boolean
profile: ProfileModel
}
-const uploadImageToS3 = async (presignedUrl: string, file: File): Promise => {
- try {
- await axiosInstance.put(presignedUrl, file, {
- headers: {
- 'Content-Type': file.type,
- },
- })
- } catch (error) {
- console.error('์ด๋ฏธ์ง ์
๋ก๋ ์คํจ:', error)
- throw new Error('์ด๋ฏธ์ง ์
๋ก๋์ ์คํจํ์ต๋๋ค')
- }
-}
const UserInfo = ({ profile, isEditable = false }: Props) => {
const router = useRouter()
-
- const initialForm: ProfileFormModel = {
- name: profile?.userName || '',
- nickname: profile?.nickname || '',
- email: profile?.email || '',
- password: '',
- passwordConfirm: '',
- phone: profile?.phone || '',
- birthDate: profile?.birthDate || '',
- }
-
- const [form, setForm] = useState(initialForm)
- const [formState, setFormState] = useState(initialFormState)
- const [errors, setErrors] = useState({})
- const [isValidated, setIsValidated] = useState(false)
- const [isNicknameModified, setIsNicknameModified] = useState(false)
- const [isPhoneModified, setIsPhoneModified] = useState(false)
- const [selectedImage, setSelectedImage] = useState(null)
- const [previewUrl, setPreviewUrl] = useState(null)
- const [isUploading, setIsUploading] = useState(false)
-
- const handleInputChange = (e: ChangeEvent) => {
- const { name, value } = e.target
- setForm((prev) => ({ ...prev, [name]: value }))
-
- if (isValidated) {
- setErrors((prev) => ({
- ...prev,
- [name]: null,
- }))
- }
-
- if (name === 'nickname') {
- setIsNicknameModified(true)
- setErrors((prev) => ({ ...prev, nickname: null }))
- }
- if (name === 'phone') {
- setIsPhoneModified(true)
- setErrors((prev) => ({ ...prev, phone: null }))
- }
- }
-
- const handleImageChange = (e: ChangeEvent) => {
- const file = e.target.files?.[0]
- if (!file) return
-
- if (!file.type.startsWith('image/')) {
- alert('์ด๋ฏธ์ง ํ์ผ๋ง ์
๋ก๋๊ฐ ๊ฐ๋ฅํฉ๋๋ค.')
- return
- }
-
- const maxSize = 5 * 1024 * 1024
- if (file.size > maxSize) {
- alert('ํ์ผ ํฌ๊ธฐ๋ 5MB ์ดํ์ฌ์ผ ํฉ๋๋ค.')
- return
- }
-
- const reader = new FileReader()
- reader.onload = (e) => {
- setPreviewUrl(e.target?.result as string)
- }
- reader.readAsDataURL(file)
-
- setSelectedImage(file)
- }
-
- const handleNicknameCheck = async () => {
- try {
- const response = await checkNicknameDuplicate(form.nickname)
- if (response.result.isAvailable) {
- setFormState((prev) => ({ ...prev, isNicknameVerified: true }))
- setIsNicknameModified(false)
- if (errors.nickname) {
- setErrors((prev) => ({ ...prev, nickname: null }))
- }
- } else {
- setErrors((prev) => ({ ...prev, nickname: SIGNUP_ERROR_MESSAGES.NICKNAME_DUPLICATED }))
- setFormState((prev) => ({ ...prev, isNicknameVerified: false }))
- }
- } catch (err) {
- console.error('๋๋ค์ ์ค๋ณต ํ์ธ ์คํจ:', err)
- setErrors((prev) => ({ ...prev, nickname: SIGNUP_ERROR_MESSAGES.NICKNAME_CHECK_FAILED }))
- setFormState((prev) => ({ ...prev, isNicknameVerified: false }))
- }
- }
-
- const handlePhoneCheck = async () => {
- try {
- const response = await checkPhoneDuplicate(form.phone)
- if (response.result.isAvailable) {
- setFormState((prev) => ({ ...prev, isPhoneVerified: true }))
- setIsPhoneModified(false)
- if (errors.phone) {
- setErrors((prev) => ({ ...prev, phone: null }))
- }
- } else {
- setErrors((prev) => ({ ...prev, phone: SIGNUP_ERROR_MESSAGES.PHONE_DUPLICATED }))
- setFormState((prev) => ({ ...prev, isPhoneVerified: false }))
- }
- } catch (err) {
- console.error('ํด๋ํฐ ๋ฒํธ ์ค๋ณต ํ์ธ ์คํจ:', err)
- setErrors((prev) => ({ ...prev, phone: SIGNUP_ERROR_MESSAGES.PHONE_CHECK_FAILED }))
- setFormState((prev) => ({ ...prev, isPhoneVerified: false }))
- }
- }
-
- const handleImageDelete = () => {
- setSelectedImage(null)
- setPreviewUrl(null)
- }
+ const updateProfile = usePatchUserProfile()
+
+ const {
+ form,
+ errors,
+ successMessages,
+ hasNicknameChanged,
+ hasPhoneChanged,
+ handleInputChange,
+ handleNicknameCheck,
+ handlePhoneCheck,
+ handlePasswordCheck,
+ validateChangedFields,
+ isFormValid,
+ getUpdatedFields,
+ } = useProfileForm(profile)
+
+ const {
+ selectedImage,
+ previewUrl,
+ imageError,
+ isImageVerified,
+ handleImageChange,
+ handleImageDelete,
+ } = useProfileImage()
const handleBack = () => {
router.back()
}
const handleFormSubmit = async () => {
- const formErrors = validateProfileForm(
- form,
- formState.isNicknameVerified,
- formState.isPhoneVerified
- )
- setIsValidated(true)
-
- if (Object.keys(formErrors).length > 0) {
- setErrors(formErrors)
- return
- }
+ if (!validateChangedFields()) return
try {
- setIsUploading(true)
-
+ const updatedFields = getUpdatedFields()
const updateData = {
- nickName: form.nickname,
- phoneNum: form.phone,
- password: form.password || null,
- email: form.email,
+ ...updatedFields,
imageChange: selectedImage !== null,
- profileImage: selectedImage
- ? {
- imageName: selectedImage.name,
- size: selectedImage.size,
- }
- : null,
- }
-
- console.log('์์ฒญ ๋ฐ์ดํฐ:', updateData)
-
- const response = await axiosInstance.patch('/api/users/mypage/profile', updateData)
-
- if (!response.data.isSuccess) {
- throw new Error(response.data.message)
- }
-
- if (selectedImage && response.data.data?.presignedUrl) {
- await uploadImageToS3(response.data.data.presignedUrl, selectedImage)
}
- alert('ํ๋กํ์ด ์ฑ๊ณต์ ์ผ๋ก ์
๋ฐ์ดํธ๋์์ต๋๋ค.')
- router.push(PATH.PROFILE)
- } catch (error) {
- console.error('ํ๋กํ ์
๋ฐ์ดํธ ์คํจ:', error)
- if (axios.isAxiosError(error)) {
- const errorMessage = error.response?.data?.message || 'ํ๋กํ ์
๋ฐ์ดํธ์ ์คํจํ์ต๋๋ค.'
- alert(errorMessage)
- } else {
- alert('ํ๋กํ ์
๋ฐ์ดํธ์ ์คํจํ์ต๋๋ค. ๋ค์ ์๋ํด์ฃผ์ธ์.')
- }
- } finally {
- setIsUploading(false)
+ await updateProfile.mutateAsync(
+ {
+ profileData: updateData,
+ imageFile: selectedImage,
+ },
+ {
+ onSuccess: () => {
+ router.push(PATH.PROFILE)
+ },
+ }
+ )
+ } catch (err) {
+ console.error('ํ๋กํ ์
๋ฐ์ดํธ ์คํจ:', err)
}
}
- if (!profile) {
- return null
- }
+ if (!profile) return null
+
+ const displayImageUrl = previewUrl || profile.imageUrl || ''
+ const shouldShowImage = Boolean(previewUrl || profile.imageUrl)
return (
๊ฐ์ธ ์ ๋ณด
-
-
- {previewUrl ? (
-

+
+ {shouldShowImage ? (
+
+
+
) : (
)}
-
-
-
-
+
+
+ )}
{isEditable && selectedImage && (
ํ๋กํ ์ฌ์ง ์ญ์
)}
+ {imageError &&
{imageError}
}
@@ -265,17 +140,32 @@ const UserInfo = ({ profile, isEditable = false }: Props) => {
๊ฐ์ธ ์ ๋ณด ์์
)}
-
-
์ด๋ฆ
-
+
+
@@ -286,7 +176,7 @@ const UserInfo = ({ profile, isEditable = false }: Props) => {
value={form.email}
className={cx('input')}
isWhiteDisabled={!isEditable}
- disabled={isEditable}
+ disabled={true}
/>
@@ -301,17 +191,16 @@ const UserInfo = ({ profile, isEditable = false }: Props) => {
className={cx('input')}
inputSize="compact"
isWhiteDisabled={!isEditable}
- errorMessage={errors.phone}
/>
- {isEditable && ํ์ธ}
+ {isEditable && (
+
+ ํ์ธ
+
+ )}
- {formState.isPhoneVerified && !isPhoneModified && (
-
์ฌ์ฉํ ์ ์๋ ํด๋ํฐ ๋ฒํธ์
๋๋ค.
- )}
- {isPhoneModified && (
-
- ํด๋ํฐ ๋ฒํธ๊ฐ ์์ ๋์์ต๋๋ค. ๋ค์ ํ์ธํด ์ฃผ์ธ์.
-
+ {errors.phone &&
{errors.phone}
}
+ {successMessages.phone && (
+
{successMessages.phone}
)}
@@ -325,7 +214,7 @@ const UserInfo = ({ profile, isEditable = false }: Props) => {
value={form.birthDate}
className={cx('input')}
isWhiteDisabled={!isEditable}
- disabled={isEditable}
+ disabled={true}
/>
@@ -340,17 +229,19 @@ const UserInfo = ({ profile, isEditable = false }: Props) => {
onChange={handleInputChange}
className={cx('input')}
isWhiteDisabled={!isEditable}
- errorMessage={errors.nickname}
/>
- {isEditable && ํ์ธ}
+ {isEditable && (
+
+ ํ์ธ
+
+ )}
- {formState.isNicknameVerified && !isNicknameModified && (
-
์ฌ์ฉํ ์ ์๋ ๋๋ค์์
๋๋ค.
- )}
- {isNicknameModified && (
-
- ๋๋ค์์ด ์์ ๋์์ต๋๋ค. ๋ค์ ์ค๋ณตํ์ธํด ์ฃผ์ธ์.
-
+ {errors.nickname &&
{errors.nickname}
}
+ {successMessages.nickname && (
+
{successMessages.nickname}
)}
@@ -369,28 +260,39 @@ const UserInfo = ({ profile, isEditable = false }: Props) => {
onChange={handleInputChange}
placeholder="๋น๋ฐ๋ฒํธ๋ฅผ ์
๋ ฅํ์ธ์"
className={cx('input')}
- errorMessage={errors.password}
/>
๋น๋ฐ๋ฒํธ ํ์ธ
-
+
+
+
+ ํ์ธ
+
+
+ {errors.password &&
{errors.password}
}
+ {successMessages.password && (
+
{successMessages.password}
+ )}
)}
{isEditable && (
- * ๋น๋ฐ๋ฒํธ๋ ๋ฌธ์, ์ซ์ ํฌํจ 6~20์๋ก ๊ตฌ์ฑ๋์ด์ผ ํฉ๋๋ค.
+ * ๋น๋ฐ๋ฒํธ๋ ์๋ฌธ๊ณผ ์ซ์๋ฅผ ํฌํจํ์ฌ 6~20์๋ก ๊ตฌ์ฑ๋์ด์ผ ํฉ๋๋ค.
)}
@@ -398,16 +300,20 @@ const UserInfo = ({ profile, isEditable = false }: Props) => {
{isEditable && (
-
+
๋ค๋ก๊ฐ๊ธฐ
- {isUploading ? '์ ์ฅ ์ค...' : '์ ์ฅํ๊ธฐ'}
+ {updateProfile.isPending ? '์ ์ฅ ์ค...' : '์ ์ฅํ๊ธฐ'}
)}
diff --git a/app/(dashboard)/my/profile/_ui/user-info/styles.module.scss b/app/(dashboard)/my/profile/_ui/user-info/styles.module.scss
index da2506e2..145db2a3 100644
--- a/app/(dashboard)/my/profile/_ui/user-info/styles.module.scss
+++ b/app/(dashboard)/my/profile/_ui/user-info/styles.module.scss
@@ -1,8 +1,9 @@
.container {
background-color: $color-white;
width: 897px;
- height: 854px;
+ height: 100%;
padding: 44px 40px;
+ border-radius: 5px;
}
.title {
@@ -34,50 +35,81 @@
display: flex;
flex-direction: column;
flex: 1;
- margin-right: 50px;
+ margin-right: 20px;
+ align-items: center;
}
.avatar-wrapper {
position: relative;
display: inline-block;
- margin-bottom: 35px;
-
- .camera-wrapper {
- position: absolute;
- bottom: 40px;
- right: 10px;
- width: 36px;
- height: 36px;
- transform: translate(50%, 50%);
- background: $color-orange-500;
- border-radius: 50%;
- border: 4px solid $color-white;
- cursor: pointer;
- display: flex;
- justify-content: center;
- align-items: center;
-
- .camera-icon {
- fill: $color-white;
- width: 18px;
- height: 18px;
- }
+ margin-bottom: 20px;
+ margin-right: 40px;
+ &.isEditable {
+ margin-right: 10px;
+ }
+}
+
+.image-container {
+ width: 150px;
+ height: 150px;
+ border-radius: 50%;
+ overflow: hidden;
+ position: relative;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.avatar-preview {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ object-position: center;
+ border-radius: 50%;
+}
+
+.camera-wrapper {
+ position: absolute;
+ bottom: 40px;
+ right: 10px;
+ width: 36px;
+ height: 36px;
+ transform: translate(50%, 50%);
+ background: $color-orange-500;
+ border-radius: 50%;
+ border: 4px solid $color-white;
+ cursor: pointer;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ .camera-icon {
+ fill: $color-white;
+ width: 18px;
+ height: 18px;
}
}
+.error-message {
+ color: $color-orange-700;
+ @include typo-b3;
+ margin-top: 8px;
+}
+
+.success-message {
+ color: $color-indigo;
+ @include typo-b3;
+ margin-top: 8px;
+}
+
.right-wrapper {
display: flex;
flex-direction: column;
+ margin-top: -10px;
.edit-button {
align-self: flex-end;
- margin-top: 20px;
- }
-
- .first-row {
- display: flex;
- flex-direction: column;
- margin-bottom: 36px;
+ margin-bottom: 20px;
}
.title {
@@ -88,34 +120,27 @@
.row {
display: flex;
- gap: 27px;
- align-items: center;
+ gap: 32px;
+ align-items: flex-start;
margin-bottom: 36px;
-
- .input {
- flex: 1;
- }
}
.password-row {
display: flex;
- gap: 27px;
- align-items: center;
+ gap: 32px;
+ align-items: flex-start;
}
}
.button-wrapper {
- margin-top: 103px;
+ margin-top: 40px;
display: flex;
justify-content: center;
gap: 32px;
width: 100%;
height: 40px;
- .left-button {
- width: 112px;
- }
-
+ .left-button,
.right-button {
width: 112px;
}
@@ -131,16 +156,16 @@
.position {
display: flex;
flex-direction: column;
- align-items: center;
- gap: 27px;
+ align-items: flex-start;
}
+
.position-wrapper {
display: flex;
+ align-items: center;
gap: 12px;
}
-.nickname-verified {
- margin-top: 0;
- @include typo-b3;
- color: $color-indigo;
+.input {
+ border-radius: 4px;
+ border: 1px solid $color-gray-300;
}
diff --git a/app/(dashboard)/my/profile/_ui/user-info/types.ts b/app/(dashboard)/my/profile/_ui/user-info/types.ts
index 05409966..4c1aed15 100644
--- a/app/(dashboard)/my/profile/_ui/user-info/types.ts
+++ b/app/(dashboard)/my/profile/_ui/user-info/types.ts
@@ -11,11 +11,13 @@ export interface ProfileFormModel {
passwordConfirm: string
phone: string
birthDate: string
+ joinDate: string
}
export interface ProfileFormStateModel {
isNicknameVerified: boolean
isPhoneVerified: boolean
+ isPasswordVerified: boolean
}
export interface ProfileFormErrorsModel {
@@ -24,3 +26,20 @@ export interface ProfileFormErrorsModel {
passwordConfirm?: ProfileErrorMessageType | null
phone?: ProfileErrorMessageType | null
}
+
+export interface UserProfileModel {
+ nickName: string
+ phoneNum: string
+ password?: string
+ email: string
+ imageChange: boolean
+}
+
+export interface ProfileModel {
+ userName: string
+ nickname: string
+ email: string
+ phone: string
+ birthDate: string
+ profileImage?: string | null
+}
diff --git a/app/(dashboard)/my/profile/_ui/user-profile/index.tsx b/app/(dashboard)/my/profile/_ui/user-profile/index.tsx
index bd4e247e..ff86fb4c 100644
--- a/app/(dashboard)/my/profile/_ui/user-profile/index.tsx
+++ b/app/(dashboard)/my/profile/_ui/user-profile/index.tsx
@@ -10,9 +10,10 @@ interface Props {
role: string
nickname: string
email: string
+ imageURL?: string | undefined
}
-const UserProfile = ({ role, nickname, email }: Props) => {
+const UserProfile = ({ role, nickname, email, imageURL }: Props) => {
return (
ํ๋กํ ์ ๋ณด
@@ -24,7 +25,7 @@ const UserProfile = ({ role, nickname, email }: Props) => {
{email}
diff --git a/app/(dashboard)/my/profile/_ui/user-profile/styles.module.scss b/app/(dashboard)/my/profile/_ui/user-profile/styles.module.scss
index f95f67fe..fcabae34 100644
--- a/app/(dashboard)/my/profile/_ui/user-profile/styles.module.scss
+++ b/app/(dashboard)/my/profile/_ui/user-profile/styles.module.scss
@@ -1,8 +1,8 @@
.container {
background-color: $color-white;
width: 375px;
- height: 280px;
padding: 44px 40px;
+ border-radius: 5px;
}
.title {
diff --git a/app/(dashboard)/my/profile/_ui/user-withdraw/index.tsx b/app/(dashboard)/my/profile/_ui/user-withdraw/index.tsx
deleted file mode 100644
index 6e443ff4..00000000
--- a/app/(dashboard)/my/profile/_ui/user-withdraw/index.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-'use client'
-
-import { useRouter } from 'next/navigation'
-
-import classNames from 'classnames/bind'
-
-import { Button } from '@/shared/ui/button'
-
-import styles from './styles.module.scss'
-
-const cx = classNames.bind(styles)
-
-const UserWithdraw = () => {
- const router = useRouter()
- const handleWithdraw = () => {}
- const handleBack = () => {
- router.back()
- }
- return (
-
-
ํ์ํํด
-
-
ํ์ ํํด ๋ฉ์ธ์ง
-
-
-
ใํํด ์ฆ์ ๋ชจ๋ ๊ฐ์ธ์ ๋ณด๊ฐ ์ญ์ ๋ฉ๋๋ค.
-
ใ๊ตฌ๋
ํ ์ ๋ต์ ๋ํ ๋ชจ๋ ๋ด์ฉ์ด ์ญ์ ๋ฉ๋๋ค.
-
-
-
-
- ๋ค๋ก๊ฐ๊ธฐ
-
-
- ํํด
-
-
-
- )
-}
-export default UserWithdraw
diff --git a/app/(dashboard)/my/profile/_ui/user-withdraw/styles.module.scss b/app/(dashboard)/my/profile/_ui/user-withdraw/styles.module.scss
deleted file mode 100644
index 35e2f626..00000000
--- a/app/(dashboard)/my/profile/_ui/user-withdraw/styles.module.scss
+++ /dev/null
@@ -1,68 +0,0 @@
-.container {
- background-color: $color-white;
- width: 897px;
- height: 854px;
- padding: 44px 40px;
-}
-
-.title {
- @include typo-b1;
- color: $color-gray-700;
- margin-bottom: 22px;
-}
-
-.line {
- width: 100%;
- height: 1px;
- background-color: $color-gray-300;
-}
-
-.message {
- margin-top: 69px;
- text-align: center;
- @include typo-h4;
- color: $color-gray-700;
-}
-
-.content {
- display: flex;
- justify-content: center;
- align-items: center;
-}
-
-.message-wrapper {
- margin-top: 51px;
- border: 1px solid $color-gray-600;
- border-radius: 6px;
- width: 569px;
- height: 206px;
- color: $color-gray-700;
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- gap: 10px;
-
- p {
- @include typo-b2;
- margin: 0;
- line-height: 1.5;
- }
-}
-
-.button-wrapper {
- margin-top: 58px;
- display: flex;
- justify-content: center;
- gap: 32px;
- width: 100%;
- height: 40px;
-
- .left-button {
- width: 112px;
- }
-
- .right-button {
- width: 112px;
- }
-}
diff --git a/app/(dashboard)/my/profile/edit/page.tsx b/app/(dashboard)/my/profile/edit/page.tsx
index 5c2b9406..94190652 100644
--- a/app/(dashboard)/my/profile/edit/page.tsx
+++ b/app/(dashboard)/my/profile/edit/page.tsx
@@ -1,19 +1,37 @@
'use client'
+import classNames from 'classnames/bind'
+
+import { PATH } from '@/shared/constants/path'
+import { LinkButton } from '@/shared/ui/link-button'
+
import useGetProfile from '../../_hooks/query/use-get-profile'
import UserInfo from '../_ui/user-info'
+import UserProfile from '../_ui/user-profile'
+import styles from './styles.module.scss'
+
+const cx = classNames.bind(styles)
const MyProfileEditPage = () => {
- const { data: profile, isLoading } = useGetProfile()
+ const { data: profile } = useGetProfile()
if (!profile) {
return null
}
return (
- <>
-
- >
+
)
}
diff --git a/app/(dashboard)/my/profile/page.module.scss b/app/(dashboard)/my/profile/edit/styles.module.scss
similarity index 71%
rename from app/(dashboard)/my/profile/page.module.scss
rename to app/(dashboard)/my/profile/edit/styles.module.scss
index 9fe14a05..6fd74ad3 100644
--- a/app/(dashboard)/my/profile/page.module.scss
+++ b/app/(dashboard)/my/profile/edit/styles.module.scss
@@ -1,5 +1,5 @@
.container {
- padding: 40px 28px;
+ padding: 20px 28px;
}
.title {
@@ -10,12 +10,15 @@
.wrapper {
display: flex;
- justify-content: space-between;
+ flex-direction: row;
+ gap: 28px;
+ height: 100%;
}
.user-profile {
display: flex;
flex-direction: column;
+ justify-content: start;
}
.link-button {
diff --git a/app/(dashboard)/my/profile/page.tsx b/app/(dashboard)/my/profile/page.tsx
index da50ca52..f3ea794a 100644
--- a/app/(dashboard)/my/profile/page.tsx
+++ b/app/(dashboard)/my/profile/page.tsx
@@ -8,12 +8,12 @@ import { LinkButton } from '@/shared/ui/link-button'
import useGetProfile from '../_hooks/query/use-get-profile'
import UserInfo from './_ui/user-info'
import UserProfile from './_ui/user-profile'
-import styles from './page.module.scss'
+import styles from './styles.module.scss'
const cx = classNames.bind(styles)
const MyProfilePage = () => {
- const { data: profile, isLoading } = useGetProfile()
+ const { data: profile } = useGetProfile()
if (!profile) {
return null
@@ -25,7 +25,12 @@ const MyProfilePage = () => {
-
+
ํํดํ๊ธฐ
diff --git a/app/(dashboard)/my/profile/styles.module.scss b/app/(dashboard)/my/profile/styles.module.scss
new file mode 100644
index 00000000..6fd74ad3
--- /dev/null
+++ b/app/(dashboard)/my/profile/styles.module.scss
@@ -0,0 +1,27 @@
+.container {
+ padding: 20px 28px;
+}
+
+.title {
+ margin-top: 40px;
+ @include typo-h4;
+ margin-bottom: 22px;
+}
+
+.wrapper {
+ display: flex;
+ flex-direction: row;
+ gap: 28px;
+ height: 100%;
+}
+
+.user-profile {
+ display: flex;
+ flex-direction: column;
+ justify-content: start;
+}
+
+.link-button {
+ align-self: flex-end;
+ margin-top: 25px;
+}
diff --git a/app/(dashboard)/my/profile/withdraw/page.tsx b/app/(dashboard)/my/profile/withdraw/page.tsx
index 178574c8..866b5629 100644
--- a/app/(dashboard)/my/profile/withdraw/page.tsx
+++ b/app/(dashboard)/my/profile/withdraw/page.tsx
@@ -1,7 +1,83 @@
-import UserWithdraw from '../_ui/user-withdraw'
+'use client'
+
+import { useRouter } from 'next/navigation'
+
+import classNames from 'classnames/bind'
+
+import useModal from '@/shared/hooks/custom/use-modal'
+import { Button } from '@/shared/ui/button'
+import WithdrawCheckModal from '@/shared/ui/modal/withdraw-check-modal'
+
+import useGetProfile from '../../_hooks/query/use-get-profile'
+import { useWithdraw } from '../../_hooks/query/use-user-withdrawal'
+import UserProfile from '../_ui/user-profile'
+import styles from './styles.module.scss'
+
+const cx = classNames.bind(styles)
const MyProfileWithdrawPage = () => {
- return
+ const router = useRouter()
+ const { mutate: withdraw, isPending } = useWithdraw()
+ const { isModalOpen, openModal, closeModal } = useModal()
+
+ const handleWithdraw = () => {
+ withdraw()
+ closeModal()
+ }
+
+ const handleBack = () => {
+ router.back()
+ }
+
+ const { data: profile } = useGetProfile()
+
+ if (!profile) {
+ return null
+ }
+
+ return (
+
+
๋์ ์ ๋ณด
+
+
+
ํ์ ํํด
+
+
ํ์ ํํด ๋ฉ์ธ์ง
+
+
ใํํด ์ฆ์ ๋ชจ๋ ๊ฐ์ธ์ ๋ณด๊ฐ ์ญ์ ๋ฉ๋๋ค.
+
ใ๊ตฌ๋
ํ ์ ๋ต์ ๋ํ ๋ชจ๋ ๋ด์ฉ์ด ์ญ์ ๋ฉ๋๋ค.
+
+
+
+
+ ๋ค๋ก๊ฐ๊ธฐ
+
+
+ {isPending ? '์ฒ๋ฆฌ์ค...' : 'ํํด'}
+
+
+
+
+
+
+
+
+
+ )
}
export default MyProfileWithdrawPage
diff --git a/app/(dashboard)/my/profile/withdraw/styles.module.scss b/app/(dashboard)/my/profile/withdraw/styles.module.scss
new file mode 100644
index 00000000..5eaed757
--- /dev/null
+++ b/app/(dashboard)/my/profile/withdraw/styles.module.scss
@@ -0,0 +1,74 @@
+.container {
+ padding: 20px 28px;
+}
+
+.title {
+ margin-top: 40px;
+ @include typo-h4;
+ margin-bottom: 22px;
+}
+
+.wrapper {
+ display: flex;
+ flex-direction: row;
+ gap: 28px;
+ height: 100%;
+}
+
+.content {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ height: 80vh;
+ background-color: $color-white;
+
+ .withdraw-title {
+ @include typo-b1;
+ padding: 32px 0;
+ margin: 0 24px 36px 24px;
+ border-bottom: 1px solid $color-gray-300;
+ }
+}
+
+.message-wrapper {
+ background-color: var(--color-gray-50);
+ padding: 24px;
+ border-radius: 8px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+}
+
+.withdraw-message {
+ @include typo-h4;
+ margin-bottom: 16px;
+}
+
+.notice-list {
+ width: 600px;
+ height: 200px;
+ border: 1px solid $color-gray-600;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+ p {
+ margin-bottom: 8px;
+ }
+}
+
+.button-wrapper {
+ display: flex;
+ gap: 24px;
+ margin-top: 24px;
+ justify-content: center;
+}
+
+.left-button {
+ width: 120px;
+}
+
+.right-button {
+ width: 120px;
+}
diff --git a/app/(dashboard)/my/questions/[questionId]/_ui/question-container/index.tsx b/app/(dashboard)/my/questions/[questionId]/_ui/question-container/index.tsx
index 9150e3f7..2e8078ec 100644
--- a/app/(dashboard)/my/questions/[questionId]/_ui/question-container/index.tsx
+++ b/app/(dashboard)/my/questions/[questionId]/_ui/question-container/index.tsx
@@ -4,7 +4,6 @@ import { useRef, useState } from 'react'
import { useParams, useRouter } from 'next/navigation'
-import usePostQuestion from '@/app/(dashboard)/strategies/[strategyId]/_hooks/query/use-post-question'
import classNames from 'classnames/bind'
import { PATH } from '@/shared/constants/path'
@@ -12,13 +11,13 @@ import { useAuthStore } from '@/shared/stores/use-auth-store'
import { Button } from '@/shared/ui/button'
import { ErrorMessage } from '@/shared/ui/error-message'
import AddQuestionModal from '@/shared/ui/modal/add-question-modal'
-import { Textarea } from '@/shared/ui/textarea'
+import QuestionDeleteModal from '@/shared/ui/modal/question-delete-modal'
+import Textarea from '@/shared/ui/textarea'
import useDeleteAnswer from '../../../_hooks/query/use-delete-answer'
import useDeleteQuestion from '../../../_hooks/query/use-delete-question'
import useGetQuestionDetails from '../../../_hooks/query/use-get-question-details'
import usePostAnswer from '../../../_hooks/query/use-post-answer'
-import QuestionDeleteModal from '../../../_ui/modal/question-delete-modal'
import QuestionDetailCard from '../question-detail-card'
import styles from './styles.module.scss'
@@ -38,7 +37,7 @@ const QuestionContainer = () => {
const { mutate: submitAnswer } = usePostAnswer(parseInt(questionId as string))
const { mutate: deleteAnswer } = useDeleteAnswer()
const { mutate: deleteQuestion } = useDeleteQuestion()
- const { mutate: postQuestion } = usePostQuestion()
+
const { data: questionDetails } = useGetQuestionDetails({
questionId: parseInt(questionId as string),
})
@@ -130,6 +129,7 @@ const QuestionContainer = () => {
title={questionDetails.title}
contents={questionDetails.content}
nickname={questionDetails.nickname}
+ profileImage={questionDetails.profileImageUrl}
createdAt={questionDetails.createdAt}
status={questionDetails.state === 'WAITING' ? '๋ต๋ณ ๋๊ธฐ' : '๋ต๋ณ ์๋ฃ'}
onDelete={handleDeleteQuestionClick}
@@ -140,6 +140,7 @@ const QuestionContainer = () => {
isAuthor={isTrader}
contents={questionDetails.answer.content}
nickname={questionDetails.answer.nickname}
+ profileImage={questionDetails.answer.profileImageUrl}
createdAt={questionDetails.answer.createdAt}
onDelete={handleDeleteAnswerClick}
/>
diff --git a/app/(dashboard)/my/questions/[questionId]/_ui/question-detail-card/index.tsx b/app/(dashboard)/my/questions/[questionId]/_ui/question-detail-card/index.tsx
index e1f21245..b3b39ee0 100644
--- a/app/(dashboard)/my/questions/[questionId]/_ui/question-detail-card/index.tsx
+++ b/app/(dashboard)/my/questions/[questionId]/_ui/question-detail-card/index.tsx
@@ -1,6 +1,6 @@
'use client'
-import React from 'react'
+import { Fragment } from 'react'
import classNames from 'classnames/bind'
@@ -62,10 +62,10 @@ const QuestionDetailCard = ({
{contents.split('\n').map((line, idx) => (
-
+
{line}
-
+
))}
diff --git a/app/(dashboard)/my/questions/[questionId]/_ui/question-detail-card/question-detail-card.stories.tsx b/app/(dashboard)/my/questions/[questionId]/_ui/question-detail-card/question-detail-card.stories.tsx
index 65d61a95..f778b621 100644
--- a/app/(dashboard)/my/questions/[questionId]/_ui/question-detail-card/question-detail-card.stories.tsx
+++ b/app/(dashboard)/my/questions/[questionId]/_ui/question-detail-card/question-detail-card.stories.tsx
@@ -24,6 +24,7 @@ Question.args = {
nickname: 'ํฌ์์ด๋ณด',
profileImage: '',
createdAt: '2024-11-03T15:00:00',
+ status: '๋ต๋ณ ๋๊ธฐ',
isAuthor: false,
onDelete: () => alert('์ญ์ ๋ฒํผ ํด๋ฆญ'),
}
diff --git a/app/(dashboard)/my/questions/_hooks/query/use-delete-answer.ts b/app/(dashboard)/my/questions/_hooks/query/use-delete-answer.ts
index 808cf3ec..475b8934 100644
--- a/app/(dashboard)/my/questions/_hooks/query/use-delete-answer.ts
+++ b/app/(dashboard)/my/questions/_hooks/query/use-delete-answer.ts
@@ -1,5 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
+import { QUERY_KEY } from '@/shared/constants/query-key'
+
import deleteAnswer, { DeleteAnswerProps } from '../../_api/delete-answer'
const useDeleteAnswer = () => {
@@ -9,11 +11,11 @@ const useDeleteAnswer = () => {
deleteAnswer({ questionId, answerId }),
onSuccess: (_, { questionId }) => {
queryClient.invalidateQueries({
- queryKey: ['questionDetails', questionId],
+ queryKey: [QUERY_KEY.QUESTION_DETAILS, questionId],
})
queryClient.invalidateQueries({
- queryKey: ['questionList'],
+ queryKey: [QUERY_KEY.QUESTION_LIST],
})
},
})
diff --git a/app/(dashboard)/my/questions/_hooks/query/use-delete-question.ts b/app/(dashboard)/my/questions/_hooks/query/use-delete-question.ts
index 85498daf..5ff10cf9 100644
--- a/app/(dashboard)/my/questions/_hooks/query/use-delete-question.ts
+++ b/app/(dashboard)/my/questions/_hooks/query/use-delete-question.ts
@@ -1,5 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
+import { QUERY_KEY } from '@/shared/constants/query-key'
+
import deleteQuestion, { DeleteQuestionProps } from '../../_api/delete-question'
const useDeleteQuestion = () => {
@@ -9,11 +11,11 @@ const useDeleteQuestion = () => {
deleteQuestion({ questionId, strategyId }),
onSuccess: (_, { questionId }) => {
queryClient.invalidateQueries({
- queryKey: ['questionDetails', questionId],
+ queryKey: [QUERY_KEY.QUESTION_DETAILS, questionId],
})
queryClient.invalidateQueries({
- queryKey: ['questionList'],
+ queryKey: [QUERY_KEY.QUESTION_LIST],
})
},
})
diff --git a/app/(dashboard)/my/questions/_hooks/query/use-get-my-question-list.ts b/app/(dashboard)/my/questions/_hooks/query/use-get-my-question-list.ts
index 2f84b48d..72bd0fdd 100644
--- a/app/(dashboard)/my/questions/_hooks/query/use-get-my-question-list.ts
+++ b/app/(dashboard)/my/questions/_hooks/query/use-get-my-question-list.ts
@@ -1,5 +1,6 @@
import { useQuery } from '@tanstack/react-query'
+import { QUERY_KEY } from '@/shared/constants/query-key'
import { UserType } from '@/shared/types/auth'
import { QuestionSearchOptionsModel } from '@/shared/types/questions'
@@ -14,7 +15,7 @@ interface Props {
const useGetMyQuestionList = ({ page, size, userType, options }: Props) => {
return useQuery({
- queryKey: ['questionList', page, size, options],
+ queryKey: [QUERY_KEY.QUESTION_LIST, page, size, options],
queryFn: () => {
const { keyword, searchCondition, stateCondition } = options
return getMyQuestionList({
diff --git a/app/(dashboard)/my/questions/_hooks/query/use-get-question-details.ts b/app/(dashboard)/my/questions/_hooks/query/use-get-question-details.ts
index 06f737ae..88847d6f 100644
--- a/app/(dashboard)/my/questions/_hooks/query/use-get-question-details.ts
+++ b/app/(dashboard)/my/questions/_hooks/query/use-get-question-details.ts
@@ -1,5 +1,7 @@
import { useQuery } from '@tanstack/react-query'
+import { QUERY_KEY } from '@/shared/constants/query-key'
+
import getQuestionDetails from '../../_api/get-question-details'
interface Props {
@@ -8,7 +10,7 @@ interface Props {
const useGetQuestionDetails = ({ questionId }: Props) => {
return useQuery({
- queryKey: ['questionDetails', questionId],
+ queryKey: [QUERY_KEY.QUESTION_DETAILS, questionId],
queryFn: () => getQuestionDetails({ questionId }),
})
}
diff --git a/app/(dashboard)/my/questions/_hooks/query/use-post-answer.ts b/app/(dashboard)/my/questions/_hooks/query/use-post-answer.ts
index 0d3a64af..c6e2acb7 100644
--- a/app/(dashboard)/my/questions/_hooks/query/use-post-answer.ts
+++ b/app/(dashboard)/my/questions/_hooks/query/use-post-answer.ts
@@ -1,5 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
+import { QUERY_KEY } from '@/shared/constants/query-key'
+
import postAnswer from '../../_api/post-answer'
const usePostAnswer = (questionId: number) => {
@@ -8,11 +10,11 @@ const usePostAnswer = (questionId: number) => {
mutationFn: (content: string) => postAnswer(questionId, content),
onSuccess: () => {
queryClient.invalidateQueries({
- queryKey: ['questionDetails', questionId],
+ queryKey: [QUERY_KEY.QUESTION_DETAILS, questionId],
})
queryClient.invalidateQueries({
- queryKey: ['questionList'],
+ queryKey: [QUERY_KEY.QUESTION_LIST],
})
},
})
diff --git a/app/(dashboard)/my/questions/_ui/questions-tab-content/index.tsx b/app/(dashboard)/my/questions/_ui/questions-tab-content/index.tsx
index e7225d35..3d39b09e 100644
--- a/app/(dashboard)/my/questions/_ui/questions-tab-content/index.tsx
+++ b/app/(dashboard)/my/questions/_ui/questions-tab-content/index.tsx
@@ -26,10 +26,12 @@ const QuestionsTabContent = ({ options }: Props) => {
const user = useAuthStore((state) => state.user)
+ const userType = user?.role.includes('TRADER') ? 'TRADER' : 'INVESTOR'
+
const { data } = useGetMyQuestionList({
page,
size: COUNT_PER_PAGE,
- userType: user?.role.includes('TRADER') ? 'TRADER' : 'INVESTOR',
+ userType,
options,
})
@@ -42,21 +44,24 @@ const QuestionsTabContent = ({ options }: Props) => {
return (
<>
- {questionsData &&
- !!questionsData.length &&
- questionsData.map((question) => (
+ {questionsData?.map((question) => {
+ const userInfo = userType === 'TRADER' ? question.investor : question.trader
+
+ return (
-
- ))}
+ )
+ })}
{(!questionsData || !questionsData.length) && (
diff --git a/app/(dashboard)/my/questions/_ui/questions-tab-content/styles.module.scss b/app/(dashboard)/my/questions/_ui/questions-tab-content/styles.module.scss
index 3517a7ec..f93b3fa7 100644
--- a/app/(dashboard)/my/questions/_ui/questions-tab-content/styles.module.scss
+++ b/app/(dashboard)/my/questions/_ui/questions-tab-content/styles.module.scss
@@ -7,8 +7,10 @@
}
.empty-message {
- margin: 12px 0;
- @include typo-b2;
+ margin-top: 180px;
+ text-align: center;
+ @include typo-b1;
+ color: $color-gray-600;
}
.pagination-wrapper {
diff --git a/app/(dashboard)/my/questions/page.tsx b/app/(dashboard)/my/questions/page.tsx
index 4c219c08..a2590400 100644
--- a/app/(dashboard)/my/questions/page.tsx
+++ b/app/(dashboard)/my/questions/page.tsx
@@ -6,7 +6,7 @@ import classNames from 'classnames/bind'
import { QuestionSearchConditionType } from '@/shared/types/questions'
import { DropdownValueType } from '@/shared/ui/dropdown/types'
-import { SearchInput } from '@/shared/ui/search-input'
+import SearchInput from '@/shared/ui/search-input'
import Select from '@/shared/ui/select'
import Title from '@/shared/ui/title'
@@ -64,7 +64,7 @@ const MyQuestionsPage = () => {