Skip to content

Commit

Permalink
Merge pull request #2 from wanted-fork-fork/feature/info-edit-page
Browse files Browse the repository at this point in the history
[Feature] 프로필 수정 페이지
  • Loading branch information
ooooorobo authored Nov 9, 2024
2 parents 5d75bb4 + d7168c7 commit 12f00c9
Show file tree
Hide file tree
Showing 20 changed files with 495 additions and 174 deletions.
26 changes: 13 additions & 13 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
"i18next-fs-backend": "^2.3.2",
"i18next-http-backend": "^2.5.2",
"isbot": "^4.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react": "18.3.0-canary-2f8f77602-20240229",
"react-dom": "18.3.0-canary-2f8f77602-20240229",
"react-error-boundary": "^4.0.13",
"react-hot-toast": "^2.4.1",
"react-i18next": "^15.0.1",
Expand Down Expand Up @@ -89,5 +89,9 @@
"vite-plugin-svgr": "^4.2.0",
"vite-tsconfig-paths": "^4.2.1",
"vitest": "^1.6.0"
},
"overrides": {
"react": "18.3.0-canary-2f8f77602-20240229",
"react-dom": "18.3.0-canary-2f8f77602-20240229"
}
}
File renamed without changes.
91 changes: 91 additions & 0 deletions src/app/routes/profile.$key.edit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { getInfo, updateInfo } from 'src/types';
import { authenticate } from 'src/app/server/authenticate';
import { useLoaderData } from '@remix-run/react';
import { MyProfileProvider } from 'src/entities/profile/model/myProfileStore';
import { IdealPartnerProvider } from 'src/entities/ideal_partner/model/idealPartnerStore';
import { useMemo } from 'react';
import { convertDtoToProfile } from 'src/entities/profile/model/convertProfileToDto';
import { convertDtoToIdealPartner } from 'src/entities/ideal_partner/model/convertIdealPartnerToDto';
import { ActionFunctionArgs, json, LoaderFunction, redirect } from '@remix-run/node';
import { commitSession } from 'src/app/server/sessions';
import { EditInfoPage } from 'src/pages/edit_info/EditInfoPage';

export const loader: LoaderFunction = async ({ request, params }) => {
const { accessToken, newSession } = await authenticate(request);

const { key } = params;

if (!key) {
throw new Response('', {
status: 404,
statusText: 'Not Found',
});
}

const { data } = await getInfo(key, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});

return json(
{ profile: data },
{
headers: {
...(newSession && { 'Set-Cookie': await commitSession(newSession) }),
},
},
);
};

export const action = async ({ request }: ActionFunctionArgs) => {
const { accessToken, newSession } = await authenticate(request);
const body = await request.formData();

const id = body.get('id') as string;

try {
await updateInfo(
// TODO: zod로 타입 체크
{
id,
userInfo: JSON.parse(body.get('userInfo') as string),
idealPartner: JSON.parse(body.get('idealPartner') as string),
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
);

return redirect(`/profile/${id}`, {
headers: {
...(newSession && { 'Set-Cookie': await commitSession(newSession) }),
},
});
} catch (e) {
console.error(e, {
userInfo: JSON.parse(body.get('userInfo') as string),
idealPartner: JSON.parse(body.get('idealPartner') as string),
});
return { status: 500 };
}
};

export default function Page() {
const { profile } = useLoaderData<typeof loader>();
const profileInitialState = useMemo(() => convertDtoToProfile(profile.userInfo), [profile.userInfo]);
const idealPartnerInitialState = useMemo(
() => (profile.idealPartner ? convertDtoToIdealPartner(profile.idealPartner) : undefined),
[profile.idealPartner],
);

return (
<MyProfileProvider initialState={profileInitialState}>
<IdealPartnerProvider initialState={idealPartnerInitialState}>
<EditInfoPage infoId={profile.id} />
</IdealPartnerProvider>
</MyProfileProvider>
);
}
37 changes: 37 additions & 0 deletions src/entities/ideal_partner/model/idealPartnerStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { Hobby } from 'src/entities/hobby/types/hobby';
import { createStoreContext } from 'src/shared/functions/createStoreContext';
import { MAX_IDEAL_HEIGHT, MIN_IDEAL_HEIGHT } from 'src/processes/ideal_partner/HeightStyleForm/HeightStyleForm';
import { DrinkingDrinkingCategory, ImageDto, ReligionReligionCategory, SmokingSmokingCategory } from 'src/types';
import { useDataUrlListFromFiles } from 'src/shared/functions/useDataUrlListFromFiles';
import { useCallback, useMemo } from 'react';

export const REQUIRED_OPTION_MAX_COUNT = 3;

Expand Down Expand Up @@ -44,6 +46,7 @@ type Action = {
setMaxHeight: (value: number) => void;
setStyle: (value: string) => void;
setImages: (getState: (prevFiles: File[]) => File[]) => void;
setImageDtoList: (getState: (prevFiles: ImageDto[]) => ImageDto[]) => void;
setLocation: (value: Location[]) => void;
setHobbies: (hobbies: Hobby[]) => void;
setReligionCategory: (category: ReligionReligionCategory) => void;
Expand Down Expand Up @@ -82,6 +85,7 @@ const createStoreHook = () =>
images: [],
imageDtoList: [],
setImages: (getState) => set({ images: getState(get().images) }),
setImageDtoList: (getState) => set({ imageDtoList: getState(get().imageDtoList) }),
locations: [],
setLocation: (locations) => set({ locations }),
hobbies: [],
Expand Down Expand Up @@ -114,3 +118,36 @@ const createStoreHook = () =>
export const [IdealPartnerProvider, useIdealPartnerStore] = createStoreContext<IdealPartner, IdealPartner & Action>(
createStoreHook,
);

export const useIdealPartnerImages = () => {
const imageFiles = useIdealPartnerStore((state) => state.images);
const imageDtoList = useIdealPartnerStore((state) => state.imageDtoList);

const urls = useDataUrlListFromFiles(imageFiles);

return useMemo(() => {
return [
...imageDtoList,
...(urls.filter(Boolean) as string[]).map(
(url, idx) =>
({
imageId: idx.toString(),
url,
}) satisfies ImageDto,
),
];
}, [imageDtoList, urls]);
};

export const useRemoveIdealPartnerImageDto = () => {
const setImageFiles = useIdealPartnerStore((state) => state.setImages);
const setImageDtoList = useIdealPartnerStore((state) => state.setImageDtoList);

return useCallback(
(targetUrl: string, fileIdx?: number) => {
setImageFiles((prev) => prev.filter((_, idx) => idx !== fileIdx));
setImageDtoList((prev) => prev.filter(({ url }) => url !== targetUrl));
},
[setImageDtoList, setImageFiles],
);
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IdealPartner } from 'src/entities/ideal_partner/model/idealPartnerStore';
import { IdealPartner, useIdealPartnerImages } from 'src/entities/ideal_partner/model/idealPartnerStore';
import { AvatarList } from 'src/shared/ui/AvatarList/AvatarList';
import { Chip } from 'src/shared/ui/Chip/Chip';
import { ProfileCellHeader } from 'src/shared/ui/Profile/ProfileCellHeader';
Expand All @@ -13,6 +13,8 @@ export const IdealPartnerProfile = ({ profile }: { profile: IdealPartner }) => {
const value = useProfileEditContext();
const onClickEdit = value.canEdit ? value.onEdit : undefined;

const imageDtoList = useIdealPartnerImages();

return (
<section className={styles.Grid}>
<div className={styles.Cell}>
Expand All @@ -34,7 +36,7 @@ export const IdealPartnerProfile = ({ profile }: { profile: IdealPartner }) => {
<div className={styles.Cell}>
<ProfileCellHeader title={'이상형 참고 사진'} onClickEdit={() => onClickEdit?.('IDEAL_HEIGHT_STYLE')} />
<span>
<AvatarList files={profile.images} />
<AvatarList imageDtoList={imageDtoList} />
</span>
</div>
<div className={styles.Cell}>
Expand Down
37 changes: 37 additions & 0 deletions src/entities/profile/model/myProfileStore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { Mbti } from 'src/shared/vo/mbti';
import { Hobby } from 'src/entities/hobby/types/hobby';
import { createStoreContext } from 'src/shared/functions/createStoreContext';
import { Book, ImageDto, JobJobCategory, Movie, ReligionReligionCategory, SmokingSmokingCategory } from 'src/types';
import { useCallback, useMemo } from 'react';
import { useDataUrlListFromFiles } from 'src/shared/functions/useDataUrlListFromFiles';

export type MyProfile = {
name: string;
Expand Down Expand Up @@ -47,6 +49,7 @@ type Action = {
setBirthDate: (date: number) => void;
setHeight: (height: number) => void;
setSelfImages: (getState: (prevFiles: File[]) => File[]) => void;
setImageDtoList: (getState: (prevFiles: ImageDto[]) => ImageDto[]) => void;
setMbti: (mbti: Mbti | null) => void;
setJobCategory: (job: JobJobCategory) => void;
setJobName: (description: string) => void;
Expand Down Expand Up @@ -91,6 +94,7 @@ const createStoreHook = (initialState?: MyProfile) =>
images: [],
imageDtoList: [],
setSelfImages: (getState) => set({ images: getState(get().images) }),
setImageDtoList: (getState) => set({ imageDtoList: getState(get().imageDtoList) }),
mbti: null,
setMbti: (mbti) => set({ mbti }),
job: {
Expand Down Expand Up @@ -149,3 +153,36 @@ const createStoreHook = (initialState?: MyProfile) =>
export const [MyProfileProvider, useMyProfileStore] = createStoreContext<MyProfile, MyProfile & Action>(
createStoreHook,
);

export const useMyProfileImages = () => {
const imageFiles = useMyProfileStore((state) => state.images);
const imageDtoList = useMyProfileStore((state) => state.imageDtoList);

const urls = useDataUrlListFromFiles(imageFiles);

return useMemo(() => {
return [
...imageDtoList,
...(urls.filter(Boolean) as string[]).map(
(url) =>
({
imageId: 'null',
url,
}) satisfies ImageDto,
),
];
}, [imageDtoList, urls]);
};

export const useRemoveProfileImageDto = () => {
const setImageFiles = useMyProfileStore((state) => state.setSelfImages);
const setImageDtoList = useMyProfileStore((state) => state.setImageDtoList);

return useCallback(
(targetUrl: string, fileIdx?: number) => {
setImageFiles((prev) => prev.filter((_, idx) => idx !== fileIdx));
setImageDtoList((prev) => prev.filter(({ url }) => url !== targetUrl));
},
[setImageDtoList, setImageFiles],
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { getJobText } from '../../../lib/getJobText';
import { getLocationText } from '../../../lib/getLocationText';
import { calculateAge, convertDateObjectToDate } from '../../../../../shared/vo/date';
import { useTranslation } from 'react-i18next';
import { MyProfile } from '../../../model/myProfileStore';
import { MyProfile, useMyProfileImages } from '../../../model/myProfileStore';
import { EditProfileFunction } from 'src/features/EditInfo/ProfileEditContext';

export const PersonalInfoGrid = ({
Expand All @@ -18,6 +18,8 @@ export const PersonalInfoGrid = ({
}) => {
const { t } = useTranslation();
const age = calculateAge(convertDateObjectToDate(profile.birthDate));
const dtoList = useMyProfileImages();

return (
<div className={styles.Grid}>
<div className={styles.GridRow}>
Expand Down Expand Up @@ -56,7 +58,7 @@ export const PersonalInfoGrid = ({
<div className={styles.Cell}>
<ProfileCellHeader title={'업로드 사진'} onClickEdit={() => onClickEdit?.('PROFILE_MY_IMAGE')} />
<div className={styles.HorizontalList}>
<AvatarList files={profile.images} />
<AvatarList imageDtoList={dtoList} />
</div>
</div>
</div>
Expand Down
51 changes: 51 additions & 0 deletions src/features/upload_image/useUploadProfileImage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { useCallback, useState } from 'react';
import { useMutation } from '@tanstack/react-query';
import { ImageDto, uploadImage } from 'src/types';

export const useUploadProfileImage = () => {
const [progress, setProgress] = useState<number>(0);

const { mutateAsync: uploadMutation } = useMutation({
mutationFn: uploadImage,
});

const uploadFileList = useCallback(
async (files: File[], onUpload: () => void) => {
const results: ImageDto[] = [];
// 한번에 너무 많이 요청하지 않도록 하나씩 업로드 요청
for (const file of files) {
try {
const result = await uploadMutation({ image: file });
onUpload();
results.push(result.data);
} catch (e) {
console.error(e);
}
}
return results;
},
[uploadMutation],
);

const upload = useCallback(
async (profileImages: File[], idealPartnerImages: File[]) => {
setProgress(0);

const total = profileImages.length + idealPartnerImages.length + 1;

const [profileImageResults, idealImageResults] = await Promise.all([
uploadFileList(profileImages, () => {
setProgress((prev) => prev + 1 / total);
}),
uploadFileList(idealPartnerImages, () => {
setProgress((prev) => prev + 1 / total);
}),
]);

return { profileImageResults, idealImageResults };
},
[uploadFileList],
);

return { progress, upload };
};
Loading

0 comments on commit 12f00c9

Please sign in to comment.