diff --git a/package.json b/package.json index fc4b30a..1f0696a 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "axios": "^1.3.2", "clsx": "^1.2.1", "dayjs": "^1.11.7", + "immer": "^9.0.19", "lottie-web": "^5.10.2", "next": "13.1.6", "query-string": "^8.1.0", diff --git a/public/assets/images/android_screenshot_1.jpg b/public/assets/images/android_screenshot_1.jpg new file mode 100644 index 0000000..36dbd6b Binary files /dev/null and b/public/assets/images/android_screenshot_1.jpg differ diff --git a/public/assets/images/android_screenshot_2.jpg b/public/assets/images/android_screenshot_2.jpg new file mode 100644 index 0000000..eda9283 Binary files /dev/null and b/public/assets/images/android_screenshot_2.jpg differ diff --git a/public/assets/images/nemoni.png b/public/assets/images/nemoni.png new file mode 100644 index 0000000..b5c97e0 Binary files /dev/null and b/public/assets/images/nemoni.png differ diff --git a/src/api/services/DayBlock.interface.ts b/src/api/services/DayBlock.interface.ts index 78c8623..b24baef 100644 --- a/src/api/services/DayBlock.interface.ts +++ b/src/api/services/DayBlock.interface.ts @@ -43,6 +43,10 @@ export default interface DayBlockService { Type.UpdateDailyReviewParams, Type.UpdateDailyReviewResponse > + deleteDailyReview: ServiceFunc< + Type.GetDailyReviewParams, + Record + > /** 태스크 */ createTaskInBlock: ServiceFunc< @@ -64,4 +68,10 @@ export default interface DayBlockService { Type.UpdateMyProfileResponse > getMyProfile: ServiceFunc + + /** 유저 */ + checkUniqueNickname: ServiceFunc< + Type.CheckUniqueNicknameParams, + Type.CheckUniqueNicknameResponse + > } diff --git a/src/api/services/DayBlockAxiosAPI.ts b/src/api/services/DayBlockAxiosAPI.ts index 33187c8..3909a00 100644 --- a/src/api/services/DayBlockAxiosAPI.ts +++ b/src/api/services/DayBlockAxiosAPI.ts @@ -26,8 +26,8 @@ export default class DayBlockAxiosAPI implements DayBlockService { updateBlock(params: Type.UpdateBlockParams) { return API.patch(`/api/block/${params.blockId}`, { title: params.title, - emoticon: params.emoticon, - blockColor: params.blockColor, + emoji: params.emoji, + backgroundColor: params.backgroundColor, isSecret: params.isSecret, }) } @@ -66,6 +66,9 @@ export default class DayBlockAxiosAPI implements DayBlockService { params, ) } + deleteDailyReview({ reviewId }: Type.GetDailyReviewParams) { + return API.delete(`/api/review/${reviewId}`) + } /** 태스크 */ createTaskInBlock(params: Type.CreateTaskInBlockParams) { @@ -75,11 +78,14 @@ export default class DayBlockAxiosAPI implements DayBlockService { ) } updateTaskInBlock(params: Type.UpdateTaskInBlockParams) { - return API.put( + return API.patch( `/api/task/${params.taskId}`, - params, + { content: params.content }, ) } + updateTaskStatus(params: Type.UpdateTaskStatusParams) { + return API.patch(`/api/task/status/${params.taskId}`) + } deleteTaskInBlock(params: Type.DeleteTaskInBlockParams) { return API.delete( `/api/task/${params.taskId}`, @@ -93,4 +99,36 @@ export default class DayBlockAxiosAPI implements DayBlockService { getMyProfile() { return API.get(`/api/user`) } + + /** 유저 */ + checkUniqueNickname({ nickname }: Type.CheckUniqueNicknameParams) { + return API.get( + `/api/user/nickname/${nickname}`, + ) + } + + /** 기타 */ + async getMyDailyBlockMetric({ + date, + }: Type.GetMyDailyBlockMetricParams): Promise { + const myProfile = await this.getMyProfile().then(({ data }) => data) + const blocks = await this.getDayBlocks({ date }).then(({ data }) => data) + + const numOfTasks = blocks.numOfTotalTasks + const numOfdoneTasks = + blocks.blocks.reduce( + (res, { numOfDoneTask }) => res + (numOfDoneTask || 0), + 0, + ) || 0 + + return { + date, + user: myProfile, + numOfBlocks: blocks.numOfTotalBlocks, + numOfTasks, + numOfdoneTasks, + percentageOfDoneTasks: + numOfTasks === 0 ? 0 : Math.round((numOfdoneTasks / numOfTasks) * 100), + } + } } diff --git a/src/api/types/base.types.ts b/src/api/types/base.types.ts index 95c6597..2fe746d 100644 --- a/src/api/types/base.types.ts +++ b/src/api/types/base.types.ts @@ -4,19 +4,16 @@ import { UserProfile } from '@/types/common.type' export type GetDailyBlocksOnWeekParams = { date: string } -export type GetDailyBlocksOnWeekResponse = { - user: string - dailyBlocks: Array -} +export type GetDailyBlocksOnWeekResponse = Array export type GetDayBlocksParams = { date: string } export type GetDayBlocksResponse = { date: string - totalBlock: number - totalTask: number - reviewId?: number + numOfTotalBlocks: number + numOfTotalTasks: number + reviewId?: number | null blocks: Array } @@ -27,12 +24,12 @@ export type GetSingleBlockParams = { export type GetSingleBlockResponse = { date: string title: string - emoticon: string - blockColor: string + emoji: string + backgroundColor: string isSecret: boolean } -export type GetSavedBlocksResponse = Omit[] +export type GetSavedBlocksResponse = Omit[] export type SaveBlockParams = { blockId: number @@ -41,8 +38,8 @@ export type SaveBlockParams = { export type CreateBlockParams = { date: string title: string - emoticon: string - blockColor: string + emoji: string + backgroundColor: string isSecret: boolean } export type CreateBlockResponse = { @@ -51,7 +48,7 @@ export type CreateBlockResponse = { export type CreateDailyReviewParams = { date: string - emoticon: string + emoji: string review: string isSecret: boolean } @@ -92,18 +89,20 @@ export type DeleteTaskInBlockResponse = unknown export type UpdateBlockParams = { blockId: number title: string - emoticon: string - blockColor: string + emoji: string + backgroundColor: string isSecret: boolean } export type UpdateBlockResponse = { title: string - emoticon: string - blockColor: string + emoji: string + backgroundColor: string isSecret: boolean } +export type UpdateTaskStatusParams = { taskId: number } + export type UpdateMyProfileParams = UserProfile export type UpdateMyProfileResponse = UserProfile @@ -113,5 +112,19 @@ export type GetMyProfileResponse = UserProfile export type DeleteSavedBlockParams = { blockId: number } export type DeleteSavedBlockResponse = Record -export type LoadSavedBlockParams = { date: string; blockId: number[] } +export type LoadSavedBlockParams = { date: string; blockIds: number[] } export type LoadSavedBlockResponse = Record + +/** 기타 */ +export type GetMyDailyBlockMetricParams = { date: string } +export type GetMyDailyBlockMetricResponse = { + date: string + user: UserProfile + numOfBlocks: number + numOfTasks: number + numOfdoneTasks: number + percentageOfDoneTasks: number +} + +export type CheckUniqueNicknameParams = { nickname: string } +export type CheckUniqueNicknameResponse = { isDuplicated: boolean } diff --git a/src/components/Block/AddTaskButton/index.tsx b/src/components/Block/AddTaskButton/index.tsx index 75c3014..b484712 100644 --- a/src/components/Block/AddTaskButton/index.tsx +++ b/src/components/Block/AddTaskButton/index.tsx @@ -1,18 +1,30 @@ import { dayBlockAPI } from '@/api' +import { CreateTaskInBlockParams } from '@/api/types/base.types' import Button from '@/components/Button' import { AddIcon } from '@/components/Icons' +import useHttpRequest from '@/hooks/useHttpRequest' +import useBlockListStore from '@/store/blocks' const AddTaskButton = ({ blockId }: { blockId: number }) => { + const addNewTaskStore = useBlockListStore((state) => state.addNewTask) + const [, createTask] = useHttpRequest((params: CreateTaskInBlockParams) => + dayBlockAPI.createTaskInBlock(params).then(({ data }) => data), + ) + const handleClick = () => { - dayBlockAPI.createTaskInBlock({ - blockId, - content: '', - }) + createTask( + { blockId, content: '' }, + { + onSuccess: ({ taskId: newTaskId }) => { + addNewTaskStore(blockId, newTaskId) + }, + }, + ) } return ( ) } +const isButtonType = (type: string): type is ButtonType => { + return !!buttonOptions.find((option) => option === type) +} + export default Header diff --git a/src/components/Input/index.tsx b/src/components/Input/index.tsx index b58932f..235e495 100644 --- a/src/components/Input/index.tsx +++ b/src/components/Input/index.tsx @@ -16,11 +16,7 @@ const ERROR_BOX_CONFIG: StatusConfig = { success: '', } -const STATUS_CONFIG: StatusConfig = { - error: 'text-sm h-6 pt-1', - default: '', - success: '', -} +const STATUS_CONFIG = 'text-sm h-6 pt-1' export interface InputProps extends ComponentProps<'input'> { showLimitCount?: boolean @@ -105,9 +101,7 @@ export default forwardRef( )} {!noStatusMessage && ( -
- {renderStatusMessage()} -
+
{renderStatusMessage()}
)} ) diff --git a/src/components/Secret/index.tsx b/src/components/Secret/index.tsx new file mode 100644 index 0000000..1a71de2 --- /dev/null +++ b/src/components/Secret/index.tsx @@ -0,0 +1,12 @@ +import clsx from 'clsx' +import { PropsWithChildren } from 'react' +import { LockIcon } from '../Icons' + +export default function Secret({ children }: PropsWithChildren) { + return ( +
+ +
{children}
+
+ ) +} diff --git a/src/constants/block.ts b/src/constants/block.ts index 2a8d12d..ba9a8cc 100644 --- a/src/constants/block.ts +++ b/src/constants/block.ts @@ -1,19 +1,18 @@ import { BlockList } from '@/types/block' -export const DAYS = ['일', '월', '화', '수', '목', '금', '토'] - export const MOCK_BLOCK_LIST: BlockList = { date: '2022-01-25', - totalBlock: 2, - totalTask: 3, + numOfTotalBlocks: 2, + numOfTotalTasks: 3, + reviewId: 1, blocks: [ { blockId: 1, - color: '#FF7154', - icon: '😀', + backgroundColor: '#FF7154', + emoji: '😀', title: '제목1', - sumOfTask: 2, - sumOfDoneTask: 1, + numOfTasks: 2, + numOfDoneTask: 1, tasks: [ { taskId: 1, @@ -29,11 +28,11 @@ export const MOCK_BLOCK_LIST: BlockList = { }, { blockId: 2, - color: '#7E85FF', - icon: '🥲', + backgroundColor: '#7E85FF', + emoji: '🥲', title: '제목2', - sumOfTask: 1, - sumOfDoneTask: 1, + numOfTasks: 1, + numOfDoneTask: 1, tasks: [ { taskId: 3, @@ -56,30 +55,30 @@ export const MOCK_COLORS: string[] = [ export const MOCK_WEEKLY_BLOCKS = [ { date: '2022-01-23', - color: MOCK_COLORS, + backgroundColor: MOCK_COLORS, }, { date: '2022-01-24', - color: MOCK_COLORS, + backgroundColor: MOCK_COLORS, }, { date: '2022-01-25', - color: MOCK_COLORS, + backgroundColor: MOCK_COLORS, }, { date: '2022-01-26', - color: MOCK_COLORS, + backgroundColor: MOCK_COLORS, }, { date: '2022-01-27', - color: MOCK_COLORS, + backgroundColor: MOCK_COLORS, }, { date: '2022-01-28', - color: MOCK_COLORS, + backgroundColor: MOCK_COLORS, }, { date: '2022-01-29', - color: MOCK_COLORS, + backgroundColor: MOCK_COLORS, }, ] diff --git a/src/containers/DailyDiary/CreateOrUpdate/index.tsx b/src/containers/DailyDiary/CreateOrUpdate/index.tsx new file mode 100644 index 0000000..ff5eff5 --- /dev/null +++ b/src/containers/DailyDiary/CreateOrUpdate/index.tsx @@ -0,0 +1,188 @@ +import { ChangeEvent, useEffect, useState } from 'react' +import clsx from 'clsx' + +import rnWebViewBridge from '@/utils/react-native-webview-bridge/new-webview/rnWebViewBridge' +import useRNEmojiBottomSheet from '@/utils/react-native-webview-bridge/bottom-sheet/useRNEmojiBottomSheet' + +import useHttpRequest from '@/hooks/useHttpRequest' +import { GetDailyReviewParams } from '@/api/types/base.types' +import { dayBlockAPI } from '@/api' + +import LoadingContainer from '@/components/Loading/Container' +import { BottomButtonLayout } from '@/components/Layout' +import Header from '@/components/Header' +import Switch from '@/components/Switch' +import { FaceIcon } from '@/components/Icons' +import DiaryTextArea from '../DiaryTextArea' +import ProfileHeader from '../ProfileHeader' + +const LABEL_STYLE = 'text-lg font-bold text-black mt-[40px] mb-[10px]' +const SUB_TITLE_STYLE = 'text-lg tracking-[-0.006em] text-black mb-[6px]' +const DESCRIPTION_STYLE = 'text-sm tracking-[-0.004em] text-textGray-100' + +const DIARY_MAX_LENGTH = 100 + +interface Props { + date: string + reviewId: number +} + +export default function CreateOrUpdatePage({ date, reviewId }: Props) { + const [open, close] = useRNEmojiBottomSheet('newBlock') + + const [metrics, getMetrics, isMetricLoading] = useHttpRequest(() => + dayBlockAPI.getMyDailyBlockMetric({ date }), + ) + + const [diary, getDiary, isGetLoading] = useHttpRequest( + (params: GetDailyReviewParams) => + dayBlockAPI.getDailyReview(params).then(({ data }) => data), + ) + const [, updateDiary, isUpdateLoading] = useHttpRequest( + dayBlockAPI.updateDailyReview, + ) + const [, createDiary, isCreateLoading] = useHttpRequest( + dayBlockAPI.createDailyReview, + ) + + const [emoji, setEmoji] = useState(diary?.emoji || '') + const [review, setReviewText] = useState(diary?.review || '') + const [isSecret, setIsSecret] = useState(diary?.isSecret || false) + const isVaild = !!emoji && !!review.length + + const handleSubmit = () => { + if (!emoji || !review?.length) { + // TODO 에러 처리 + return + } + + if (!Number.isNaN(reviewId)) { + updateDiary( + { reviewId, date, emoji, review, isSecret }, + { + onSuccess: () => handleGoBack(), + }, + ) + } else { + createDiary( + { date, emoji, review, isSecret }, + { + onSuccess: () => handleGoBack(), + }, + ) + } + } + + const handleGoBack = () => { + rnWebViewBridge.close() + } + + const handleSecretChange = (value: boolean) => { + setIsSecret(value) + } + + const handleDiaryChange = ({ target }: ChangeEvent) => { + setReviewText((target?.value || '').slice(0, DIARY_MAX_LENGTH)) + } + + const handleEmojiClick = () => { + open({ + onItemClick: (key) => { + setEmoji(key) + close() + }, + }) + } + + useEffect(() => { + if (!Number.isNaN(reviewId)) { + getDiary( + { reviewId }, + { + onSuccess: ({ emoji, review, isSecret }) => { + setEmoji(emoji) + setReviewText(review) + setIsSecret(isSecret) + }, + }, + ) + } + }, [reviewId]) + + useEffect(() => { + if (date) { + getMetrics() + } + }, [date]) + + return ( + + + +
+
+ +
오늘의 감정
+ +
+
하루 일기
+
+ {review.length}/{DIARY_MAX_LENGTH} +
+
+ +
추가 설정
+
+
+
쉿! 비밀로 하기
+
+ 친구들에게 보이지 않아요 +
+
+ +
+
+ + + ) +} diff --git a/src/containers/DailyDiary/ProfileHeader/index.tsx b/src/containers/DailyDiary/ProfileHeader/index.tsx index 953ec0b..8a125d4 100644 --- a/src/containers/DailyDiary/ProfileHeader/index.tsx +++ b/src/containers/DailyDiary/ProfileHeader/index.tsx @@ -1,56 +1,29 @@ -import { useEffect } from 'react' - -import { dayBlockAPI } from '@/api' -import useHttpRequest from '@/hooks/useHttpRequest' - import { DEFAULT_PROFILE_IMAGE_URL } from '@/constants/urls' import formatDate from '@/utils/formatDate' import PercentageProfile from '@/components/PercentageProfile' +import { GetMyDailyBlockMetricResponse } from '@/api/types/base.types' interface Props { - date: string + metrics?: GetMyDailyBlockMetricResponse } -const ProfileHeader = ({ date }: Props) => { - const [myProfile, getMyprofile] = useHttpRequest(() => - dayBlockAPI.getMyProfile().then(({ data }) => data), - ) - const [blocks, getBlocks] = useHttpRequest(() => - dayBlockAPI.getDayBlocks({ date }).then(({ data }) => data), - ) - const numOfdoneTasks = - blocks?.blocks?.reduce( - (res, { sumOfDoneTask }) => res + sumOfDoneTask, - 0, - ) || 0 - - useEffect(() => { - getMyprofile() - if (date) { - getBlocks() - } - }, [date]) - +const ProfileHeader = ({ metrics }: Props) => { return (
- {formatDate(date)} + {formatDate(metrics?.date)}
- 블럭 {blocks?.totalBlock ?? '-'}개, 할 일 {blocks?.totalTask ?? '-'} - 개 + 블럭 {metrics?.numOfBlocks ?? '-'}개, 할 일{' '} + {metrics?.numOfTasks ?? '-'}개
diff --git a/src/containers/DailyDiary/ViewMode/index.tsx b/src/containers/DailyDiary/ViewMode/index.tsx new file mode 100644 index 0000000..c4c860c --- /dev/null +++ b/src/containers/DailyDiary/ViewMode/index.tsx @@ -0,0 +1,95 @@ +import { useEffect } from 'react' +import clsx from 'clsx' + +import { dayBlockAPI } from '@/api' +import { GetDailyReviewParams } from '@/api/types/base.types' +import useHttpRequest from '@/hooks/useHttpRequest' + +import rnWebViewBridge from '@/utils/react-native-webview-bridge/new-webview/rnWebViewBridge' + +import Header from '@/components/Header' +import { BottomButtonLayout } from '@/components/Layout' +import LoadingContainer from '@/components/Loading/Container' +import Secret from '@/components/Secret' +import ProfileHeader from '../ProfileHeader' + +const LABEL_STYLE = 'text-lg font-bold text-black mt-[40px] mb-[10px]' + +interface Props { + reviewId: number + date: string + onEditClick: () => void +} + +export default function ViewModePage({ reviewId, date, onEditClick }: Props) { + const [diary, getDiary, isGetLoading] = useHttpRequest( + (params: GetDailyReviewParams) => + dayBlockAPI.getDailyReview(params).then(({ data }) => data), + ) + + const [metrics, getMetrics, isMetricLoading] = useHttpRequest(() => + dayBlockAPI.getMyDailyBlockMetric({ date }), + ) + const [, deleteReview, isDeleteLoading] = useHttpRequest(() => + dayBlockAPI.deleteDailyReview({ reviewId }), + ) + + console.log(metrics) + + const handleDeleteClick = () => { + deleteReview(undefined, { + onSuccess: () => { + rnWebViewBridge.close() + }, + }) + } + + const handleGoBack = () => { + rnWebViewBridge.close() + } + + useEffect(() => { + getDiary({ reviewId }) + getMetrics() + }, []) + + return ( + + + +
+
+ +
+ {diary?.isSecret && ( + 이 글은 친구들에게 보이지 않아요 + )} +
+
오늘의 감정
+
{diary?.emoji}
+
+
+
하루 일기
+
+ {diary?.review} +
+
+
+
+
+
+ ) +} diff --git a/src/containers/DailyDiary/index.tsx b/src/containers/DailyDiary/index.tsx index 4c4165b..5e63cbe 100644 --- a/src/containers/DailyDiary/index.tsx +++ b/src/containers/DailyDiary/index.tsx @@ -1,174 +1,38 @@ -import { ChangeEvent, useEffect, useState } from 'react' +import { useState } from 'react' import { useRouter } from 'next/router' -import clsx from 'clsx' import rnWebViewBridge from '@/utils/react-native-webview-bridge/new-webview/rnWebViewBridge' -import useRNEmojiBottomSheet from '@/utils/react-native-webview-bridge/bottom-sheet/useRNEmojiBottomSheet' -import useHttpRequest from '@/hooks/useHttpRequest' -import { GetDailyReviewParams } from '@/api/types/base.types' -import { dayBlockAPI } from '@/api' - -import LoadingContainer from '@/components/Loading/Container' -import { BottomButtonLayout } from '@/components/Layout' -import Header from '@/components/Header' -import Switch from '@/components/Switch' -import { FaceIcon } from '@/components/Icons' -import DiaryTextArea from './DiaryTextArea' -import ProfileHeader from './ProfileHeader' - -const LABEL_STYLE = 'text-lg font-bold text-black mt-[40px] mb-[10px]' -const SUB_TITLE_STYLE = 'text-lg tracking-[-0.006em] text-black mb-[6px]' -const DESCRIPTION_STYLE = 'text-sm tracking-[-0.004em] text-textGray-100' - -const DIARY_MAX_LENGTH = 100 +import CreateOrUpdatePage from './CreateOrUpdate' +import ViewModePage from './ViewMode' export default function DailyDiaryContainer() { const router = useRouter() const date = router.query?.date as string const reviewId = +(router.query?.reviewId as string) - const [open, close] = useRNEmojiBottomSheet('newBlock') - - const [diary, getDiary, isGetLoading] = useHttpRequest( - (params: GetDailyReviewParams) => - dayBlockAPI.getDailyReview(params).then(({ data }) => data), - ) - const [, updateDiary, isUpdateLoading] = useHttpRequest( - dayBlockAPI.updateDailyReview, - ) - const [, createDiary, isCreateLoading] = useHttpRequest( - dayBlockAPI.createDailyReview, - ) - - const [emoji, setEmoji] = useState(diary?.emoticon || '') - const [review, setReviewText] = useState(diary?.review || '') - const [isSecret, setIsSecret] = useState(diary?.isSecret || false) - const isVaild = !!emoji && !!review.length + const [isEdited, setIsEdited] = useState(false) - const handleSubmit = () => { - if (!emoji || !review?.length) { - // TODO 에러 처리 - return - } - - if (!Number.isNaN(reviewId)) { - updateDiary( - { reviewId, date, emoticon: emoji, review, isSecret }, - { - onSuccess: () => handleGoBack(), - }, - ) - } else { - createDiary( - { date, emoticon: emoji, review, isSecret }, - { - onSuccess: () => handleGoBack(), - }, - ) - } + const handleEditClick = () => { + setIsEdited(true) } - const handleGoBack = () => { + if (!date) { rnWebViewBridge.close() + return null } - const handleSecretChange = (value: boolean) => { - setIsSecret(value) - } - - const handleDiaryChange = ({ target }: ChangeEvent) => { - setReviewText((target?.value || '').slice(0, DIARY_MAX_LENGTH)) - } - - const handleEmojiClick = () => { - open({ - onItemClick: (key) => { - setEmoji(key) - close() - }, - }) - } - - useEffect(() => { - if (!Number.isNaN(reviewId)) { - getDiary( - { reviewId }, - { - onSuccess: ({ emoticon }) => setEmoji(emoticon), - }, - ) - } - }, [reviewId]) - return ( - - - -
+ {Number.isNaN(reviewId) || isEdited ? ( + + ) : ( + -
- -
오늘의 감정
- -
-
하루 일기
-
- {review.length}/{DIARY_MAX_LENGTH} -
-
- -
추가 설정
-
-
-
쉿! 비밀로 하기
-
- 친구들에게 보이지 않아요 -
-
- -
-
- - + )} + ) } diff --git a/src/containers/Home/BlockList/DiaryButton.tsx b/src/containers/Home/BlockList/DiaryButton.tsx index 4769bc6..103b465 100644 --- a/src/containers/Home/BlockList/DiaryButton.tsx +++ b/src/containers/Home/BlockList/DiaryButton.tsx @@ -9,7 +9,7 @@ import qs from 'query-string' interface Props { date: string - reviewId?: number + reviewId?: number | null } const DiaryButton = ({ date, reviewId }: Props) => { @@ -35,7 +35,7 @@ const DiaryButton = ({ date, reviewId }: Props) => {
diff --git a/src/containers/Home/DailyBlockPanel/WeeklyBlocks.tsx b/src/containers/Home/DailyBlockPanel/WeeklyBlocks.tsx index d2f77f7..dd67823 100644 --- a/src/containers/Home/DailyBlockPanel/WeeklyBlocks.tsx +++ b/src/containers/Home/DailyBlockPanel/WeeklyBlocks.tsx @@ -1,11 +1,13 @@ -import { useState } from 'react' -import dayjs from 'dayjs' import clsx from 'clsx' +import dayjs from 'dayjs' import DailyBlock from '@/components/DailyBlock' -import { DAYS } from '@/constants/block' +import type { DailyBlock as DailyBlockType } from '@/types/block' import { noop } from '@/utils' import useSelectedDateState from '@/store/selectedDate' import { GetDailyBlocksOnWeekResponse } from '@/api/types/base.types' +import 'dayjs/locale/ko' + +dayjs.locale('ko') // Korean locale const DayBlock = ({ colors, @@ -13,7 +15,7 @@ const DayBlock = ({ isActive, onClick = noop, }: { - colors: string[] + colors: DailyBlockType['backgroundColors'] date: number isActive: boolean onClick?: () => void @@ -53,32 +55,31 @@ const DayBlock = ({ const DailyBlockPanel = ({ weeklyBlocks, }: { - weeklyBlocks: GetDailyBlocksOnWeekResponse['dailyBlocks'] + weeklyBlocks: GetDailyBlocksOnWeekResponse }) => { - const todayIdx = dayjs().day() % 7 - const [activeBlockIdx, setActiveBlockIdx] = useState(todayIdx) const setSelectedDate = useSelectedDateState((state) => state.setSelectedDate) + const selectedDate = useSelectedDateState((state) => state.date) + const selectedDay = dayjs(selectedDate).format('dd') - const handleBlockClick = (formattedDate: string, idx: number) => { + const handleBlockClick = (formattedDate: string) => { setSelectedDate(formattedDate) - setActiveBlockIdx(idx) } return (
- {weeklyBlocks.map(({ date: dateTime, color }, idx) => { + {weeklyBlocks.map(({ date: dateTime, backgroundColors }, idx) => { const formattedDate = dayjs(dateTime).format('YYYY-MM-DD') - const day = dayjs(dateTime).day() + const day = dayjs(dateTime).format('dd') const date = dayjs(dateTime).date() return (
-

{DAYS[day % 7]}

+

{day}

handleBlockClick(formattedDate, idx)} + isActive={selectedDay === day} + onClick={() => handleBlockClick(formattedDate)} />
) diff --git a/src/containers/Home/DailyBlockPanel/index.tsx b/src/containers/Home/DailyBlockPanel/index.tsx index f9bd6dd..d7ac08f 100644 --- a/src/containers/Home/DailyBlockPanel/index.tsx +++ b/src/containers/Home/DailyBlockPanel/index.tsx @@ -1,20 +1,11 @@ -import { useEffect } from 'react' -import dayjs from 'dayjs' import { GetDailyBlocksOnWeekResponse } from '@/api/types/base.types' -import useSelectedDateState from '@/store/selectedDate' import WeeklyBlocks from './WeeklyBlocks' const DailyBlockPanel = ({ dailyBlocks, }: { - dailyBlocks: GetDailyBlocksOnWeekResponse['dailyBlocks'] + dailyBlocks: GetDailyBlocksOnWeekResponse }) => { - const setSelectedDate = useSelectedDateState((state) => state.setSelectedDate) - useEffect(() => { - const today = String(dayjs().format('YYYY-MM-DD')) - setSelectedDate(today) - }, [setSelectedDate]) - return } export default DailyBlockPanel diff --git a/src/containers/Home/ProfileHeader/index.tsx b/src/containers/Home/ProfileHeader/index.tsx index 07bb529..0a119ae 100644 --- a/src/containers/Home/ProfileHeader/index.tsx +++ b/src/containers/Home/ProfileHeader/index.tsx @@ -1,20 +1,28 @@ -import { GetDailyBlocksOnWeekResponse } from '@/api/types/base.types' +import { useEffect } from 'react' +import { dayBlockAPI } from '@/api' import PercentageProfile from '@/components/PercentageProfile' +import useHttpRequest from '@/hooks/useHttpRequest' -interface Props { - user: GetDailyBlocksOnWeekResponse['user'] - profileImage?: string -} +const ProfileHeader = () => { + const [myProfile, fetchMyProfile, isLoading] = useHttpRequest(() => + dayBlockAPI.getMyProfile().then(({ data }) => data), + ) + + useEffect(() => { + fetchMyProfile() + }, []) + + if (!myProfile || isLoading) return null + const { nickname, imgUrl } = myProfile -const ProfileHeader = ({ user, profileImage = '' }: Props) => { return (
- +
- {user}님,
+ {nickname}님,
오늘 하루도 화이팅!
diff --git a/src/containers/Home/index.tsx b/src/containers/Home/index.tsx index 293ca9e..145a1ba 100644 --- a/src/containers/Home/index.tsx +++ b/src/containers/Home/index.tsx @@ -1,22 +1,27 @@ import { useEffect } from 'react' import { dayBlockAPI } from '@/api' -import dayjs from 'dayjs' import Tabs from '@/components/Tabs' import ProfileHeader from './ProfileHeader' import CalendarPanel from './CalendarPanel' import DailyBlockPanel from './DailyBlockPanel' import BlockList from './BlockList' import useHttpRequest from '@/hooks/useHttpRequest' +import useSelectedDateState from '@/store/selectedDate' +import LoadingContainer from '@/components/Loading/Container' const Home = () => { - const today = String(dayjs().format('YYYY-MM-DD')) - const [weeklyBlocks, fetchWeeklyBlocks, isLoading] = useHttpRequest(() => - dayBlockAPI.getDailyBlocksOnWeek({ date: today }).then(({ data }) => data), - ) + const selectedDate = useSelectedDateState((state) => state.date) + + const [weeklyBlocks, fetchWeeklyBlocks, isLoading, , isFetch] = + useHttpRequest(() => + dayBlockAPI + .getDailyBlocksOnWeek({ date: selectedDate }) + .then(({ data }) => data), + ) useEffect(() => { fetchWeeklyBlocks() - }, []) + }, [selectedDate]) const onVisibility = () => { if (!document.hidden) { @@ -32,12 +37,11 @@ const Home = () => { } }) - if (!weeklyBlocks || isLoading) return null - const { user, dailyBlocks } = weeklyBlocks + if (!weeklyBlocks) return null return (
- + @@ -45,14 +49,17 @@ const Home = () => { - + + + + - +
) } diff --git a/src/containers/Main/index.tsx b/src/containers/Main/index.tsx index 5cfa13e..f8acea5 100644 --- a/src/containers/Main/index.tsx +++ b/src/containers/Main/index.tsx @@ -1,49 +1,48 @@ +import Button from '@/components/Button' import clsx from 'clsx' +const TITLE_STYLE = 'text-3xl font-[600] mb-[10px]' +const DESCRIPTION_STYLE = 'mb-[6px]' + +const APP_DOWNLOAD_URL = + 'https://drive.google.com/file/d/1Af7wHA7Lf4H0De9qJrAkv7yvoENbdMli/view?usp=sharing' + const Main = () => { + const handleButtonClick = () => { + window.open(APP_DOWNLOAD_URL, '_blank') + } + return (
-
-
-
-

- 복잡한 일상을 심플하게 -

-

하루블럭

-
- -
- -
-
문의 : harublock.app@gmail.com
+
+ 관심가져 주셔서 +
+ 감사합니다🙏 +
+
+ +
+
+ +
+
+
앱 다운 후, 확인해주세요
+
+ 1. 출처를 알 수 없는 앱 설치 허용하기
+ +
+
2. Google Play 프로텍트 꺼두기
+
) diff --git a/src/containers/NewBlock/index.tsx b/src/containers/NewBlock/index.tsx index 38ee328..f54708c 100644 --- a/src/containers/NewBlock/index.tsx +++ b/src/containers/NewBlock/index.tsx @@ -37,9 +37,10 @@ export default function NewBlockContainer() { } = useRouter() const dateValue = date?.toString() const [blockTitle, setBlockTitle] = useState('') - const [emoticon, setEmoticon] = useState() - const [blockColor, setBlockColor] = useState(colors?.red) + const [emoji, setEmoticon] = useState() + const [backgroundColor, setBlockColor] = useState(colors?.red) const [isSecret, setIsSecret] = useState(false) + const [, createBlock, isCreateLoading] = useHttpRequest( (params: CreateBlockParams) => dayBlockAPI.createBlock(params).then(({ data }) => data), @@ -53,8 +54,8 @@ export default function NewBlockContainer() { setEmoticon(emoji) } - const handleColorChange = (color: string) => { - setBlockColor(color) + const handleColorChange = (backgroundColor: string) => { + setBlockColor(backgroundColor) } const handleSecretChange = (value: boolean) => { @@ -66,7 +67,7 @@ export default function NewBlockContainer() { } const handleSubmit = () => { - if (!dateValue || !emoticon || !blockColor) { + if (!dateValue || !emoji || !backgroundColor) { console.log('입력 에러') // TODO: 에러 처리 return } @@ -74,12 +75,14 @@ export default function NewBlockContainer() { { date: dateValue, title: blockTitle, - emoticon, - blockColor, + emoji, + backgroundColor, isSecret, }, { - onSuccess: () => rnWebViewBridge.close(), + onSuccess: () => { + rnWebViewBridge.close() + }, onError: () => console.log('error'), // TODO: 에러 처리 }, ) @@ -93,7 +96,7 @@ export default function NewBlockContainer() { buttonProps={{ type: 'submit', onClick: handleSubmit, - disabled: !(blockTitle && emoticon && blockColor), + disabled: !(blockTitle && emoji && backgroundColor), }} >
data), ) - const isValid = !!value?.imgPath && !!value?.user && !!value?.introduction + const isValid = !!value?.imgUrl && !!value?.nickname && !!value?.introduction const handleValueChange = (forms: UserProfile) => { setValue(forms) @@ -62,6 +62,7 @@ export default function EditProfileContainer() { onSuccess: () => { fetchMyProfile() setIsEdited(false) + rnWebViewBridge.close() }, onError: () => { // TODO 에러 처리 diff --git a/src/containers/Profile/MyProfile/ProfileHeader/index.tsx b/src/containers/Profile/MyProfile/ProfileHeader/index.tsx index df34209..f896d6b 100644 --- a/src/containers/Profile/MyProfile/ProfileHeader/index.tsx +++ b/src/containers/Profile/MyProfile/ProfileHeader/index.tsx @@ -3,10 +3,10 @@ import PercentageProfile, { } from '@/components/PercentageProfile' interface Props extends PercentageProfileProps { - user: string + nickname: string } -const ProfileHeader = ({ user, ...props }: Props) => { +const ProfileHeader = ({ nickname, ...props }: Props) => { return (
@@ -14,7 +14,7 @@ const ProfileHeader = ({ user, ...props }: Props) => {
- {user}님,
+ {nickname}님,
안녕하세요 반가워요
diff --git a/src/containers/Profile/MyProfile/index.tsx b/src/containers/Profile/MyProfile/index.tsx index 19bd3ee..f3a5c1c 100644 --- a/src/containers/Profile/MyProfile/index.tsx +++ b/src/containers/Profile/MyProfile/index.tsx @@ -57,8 +57,8 @@ export default function MyProfileContainer() {
+
-
-
-
- 프로필 생성 완료! -
-
+
+
+
+ 프로필 생성 완료! +
+
+ {myProfile?.nickname}님, 하루 블럭에 +
+ 오신 것을 환영해요 +
+ {myProfile?.imgUrl?.includes( + '/onboarding/default_profile_image.png', + ) ? ( + + ) : ( + )} - > - {myProfile?.user}님, 하루 블럭에 -
- 오신 것을 환영해요
- {myProfile?.imgPath?.includes( - '/onboarding/default_profile_image.png', - ) ? ( - - ) : ( - - )} +
+
+
-
- -
-
+ ) } diff --git a/src/containers/Profile/New/index.tsx b/src/containers/Profile/New/index.tsx index 1eb64dd..4984d52 100644 --- a/src/containers/Profile/New/index.tsx +++ b/src/containers/Profile/New/index.tsx @@ -24,14 +24,15 @@ export default function NewProfileContainer() { ) const [value, setValue] = useState() - const isValid = !!value?.imgPath && !!value?.user && !!value?.introduction + const [isValid, setValid] = useState(false) + const isFilled = !!value?.imgUrl && !!value?.nickname && !!value?.introduction const handleValueChange = (forms: UserProfile) => { setValue(forms) } const handleSubmit = () => { - if (!isValid) return + if (!isValid || !isFilled) return updateProfile(value, { onSuccess: () => { @@ -47,6 +48,10 @@ export default function NewProfileContainer() { }) } + const handleValidationChange = (validation: boolean) => { + setValid(validation) + } + const handleGoBack = () => { rnWebViewBridge.close() } @@ -58,7 +63,7 @@ export default function NewProfileContainer() { buttonText="완료" buttonProps={{ onClick: handleSubmit, - disabled: isUpdateLoading || !isValid, + disabled: isUpdateLoading || !isValid || !isFilled, }} >
프로필을 등록해보세요
- +
diff --git a/src/containers/Profile/ProfileForm/index.tsx b/src/containers/Profile/ProfileForm/index.tsx index 9a71fa8..4df15f5 100644 --- a/src/containers/Profile/ProfileForm/index.tsx +++ b/src/containers/Profile/ProfileForm/index.tsx @@ -1,4 +1,4 @@ -import { ChangeEvent, useState } from 'react' +import { ChangeEvent, FocusEvent, useState } from 'react' import clsx from 'clsx' import ProfilePlusSvg from 'public/assets/svgs/profile_plus.svg' @@ -11,38 +11,57 @@ import { UserProfile } from '@/types/common.type' import Profile from '@/components/Profile' import Switch from '@/components/Switch' import Input from '@/components/Input' +import { Status } from '@/components/Input/types' +import useHttpRequest from '@/hooks/useHttpRequest' +import { dayBlockAPI } from '@/api' +import { CheckUniqueNicknameParams } from '@/api/types/base.types' const LABEL = 'text-lg font-bold text-black mt-[40px] mb-[10px]' interface Props { defaultValue?: UserProfile + onValidationChange?: (isValid: boolean) => void onFormChange?: (values: UserProfile) => void } -export default function ProfileForm({ onFormChange, defaultValue }: Props) { +export default function ProfileForm({ + onFormChange, + onValidationChange, + defaultValue, +}: Props) { const openImagePicker = useRNImagePicker('profile') const [profileImageUrl, setImageUrl] = useState( - defaultValue?.imgPath || DEFAULT_PROFILE_IMAGE_URL, + defaultValue?.imgUrl || DEFAULT_PROFILE_IMAGE_URL, ) - const [nickname, setNickname] = useState(defaultValue?.user || '') + const [nickname, setNickname] = useState(defaultValue?.nickname || '') const [introduction, setIntroduction] = useState( defaultValue?.introduction || '', ) - const [isSecret, setIsSecret] = useState(defaultValue?.lock || false) + const [isSecret, setIsSecret] = useState(defaultValue?.isSecret || false) + + const [nicknameValidation, setNicknameValidation] = useState<{ + statusMessage: string + status: Status + }>({ statusMessage: '', status: 'default' }) + + const [, checkUniqueNickname] = useHttpRequest( + (params: CheckUniqueNicknameParams) => + dayBlockAPI.checkUniqueNickname(params).then(({ data }) => data), + ) const handleProfileImageChangeClick = () => { openImagePicker({ onImagePick: (data) => { setImageUrl(data) - handleValueChange({ imgPath: data }) + handleValueChange({ imgUrl: data }) }, }) } const handleNickNameChange = ({ target }: ChangeEvent) => { setNickname(target.value) - handleValueChange({ user: target.value }) + handleValueChange({ nickname: target.value }) } const handleDescritionChange = ({ target, @@ -52,19 +71,42 @@ export default function ProfileForm({ onFormChange, defaultValue }: Props) { } const handlePublicNickNameChange = (value: boolean) => { setIsSecret(value) - handleValueChange({ lock: value }) + handleValueChange({ isSecret: value }) } const handleValueChange = (value: Partial) => { onFormChange?.({ - imgPath: profileImageUrl, - user: nickname, + imgUrl: profileImageUrl, + nickname: nickname, introduction: introduction, - lock: isSecret, + isSecret: isSecret, ...value, }) } + const handleBlur = ({ target }: FocusEvent) => { + checkUniqueNickname( + { nickname: target.value }, + { + onSuccess: ({ isDuplicated }) => { + onValidationChange?.(!isDuplicated) + + if (isDuplicated) { + setNicknameValidation({ + statusMessage: '이미 사용 중인 닉네임입니다', + status: 'error', + }) + } else { + setNicknameValidation({ + statusMessage: '사용하실 수 있는 닉네임입니다', + status: 'success', + }) + } + }, + }, + ) + } + return ( <>
@@ -92,7 +134,9 @@ export default function ProfileForm({ onFormChange, defaultValue }: Props) { showLimitCount maxLength={6} placeholder="한글 6자 이내/특수문자 입력 불가" - defaultValue={defaultValue?.user} + defaultValue={defaultValue?.nickname} + onBlur={handleBlur} + {...nicknameValidation} />
한 줄 소개
닉네임 검색 허용
diff --git a/src/containers/SavedBlock/SavedBlock/index.stories.tsx b/src/containers/SavedBlock/SavedBlock/index.stories.tsx index e966d1f..4d3ac13 100644 --- a/src/containers/SavedBlock/SavedBlock/index.stories.tsx +++ b/src/containers/SavedBlock/SavedBlock/index.stories.tsx @@ -9,18 +9,18 @@ export default { const Template: ComponentStory = (args) => const MOCK_DATA = { - color: '#FF7154', - icon: '😂', + backgroundColor: '#FF7154', + emoji: '😂', title: '출근 준비', - sumOfTask: 4, + numOfTasks: 4, } -const { color, icon, title, sumOfTask } = MOCK_DATA +const { backgroundColor, emoji, title, numOfTasks } = MOCK_DATA export const SavedBlock = Template.bind({}) SavedBlock.args = { - color, - icon, + backgroundColor, + emoji, title, - sumOfTask, + numOfTasks, } diff --git a/src/containers/SavedBlock/SavedBlock/index.tsx b/src/containers/SavedBlock/SavedBlock/index.tsx index ecabce3..9b82706 100644 --- a/src/containers/SavedBlock/SavedBlock/index.tsx +++ b/src/containers/SavedBlock/SavedBlock/index.tsx @@ -5,7 +5,7 @@ import { SavedBlock as SavedBlockType } from '@/types/block' import { MoreVerticalIcon } from '@/components/Icons' import CheckBox from '@/components/CheckBox' -const BlockIcon = ({ icon }: { icon: string }) => { +const BlockIcon = ({ emoji }: { emoji: string }) => { return (
{ 'bg-white', )} > - {icon} + {emoji}
) } @@ -58,14 +58,14 @@ const SavedBlock = ({ 'text-white', 'w-full', )} - style={{ backgroundColor: blockData.color }} + style={{ backgroundColor: blockData.backgroundColor }} >
- +

{blockData.title}

-

{blockData.sumOfTask}

+

{blockData.numOfTasks}