Repository navigation
[Feature/#147] 마이페이지 UI구현 #170
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 7 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
7936ddf
feat: 마이페이지 구조 UI
jjjsun 5f58bbb
feat: 기본 정보 UI 구현중
jjjsun 7150ee7
feat: 마이페이지 UI 구현
jjjsun 30093bb
feat: 프로필 섹션 디자인 수정
jjjsun c388674
style: 프로필이미지 텍스트 반응형 위치 변경
jjjsun 8eadf11
fix: lint에러수정, 전화번호 추가, 기존/새 비밀번호 다르도록 로직 추가
jjjsun 3ed2ba3
fix: 코드래빗 수정사항 반영
jjjsun 3f2a2ec
Merge branch 'develop' into feature/#147
jjjsun 55692a7
chore: pageHeader삭제, profileSection내 조직 직책 직관성 향상
jjjsun File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| import { useState } from "react"; | ||
|
|
||
| import Input from "../common/input/Input"; | ||
|
|
||
| import EyeIcon from "@/assets/icon/common/eye.svg?react"; | ||
| import EyeOffIcon from "@/assets/icon/common/eye-off.svg?react"; | ||
| import LockIcon from "@/assets/icon/common/lock.svg?react"; | ||
|
|
||
| type TPasswordSectionProps = { | ||
| currentPassword: string; | ||
| setCurrentPassword: (v: string) => void; | ||
| newPassword: string; | ||
| setNewPassword: (v: string) => void; | ||
| confirmNewPassword: string; | ||
| setConfirmNewPassword: (v: string) => void; | ||
| errors: { | ||
| currentPassword: string; | ||
| newPassword: string; | ||
| confirmNewPassword: string; | ||
| }; | ||
| }; | ||
|
|
||
| export default function PasswordSection({ | ||
| currentPassword, | ||
| setCurrentPassword, | ||
| newPassword, | ||
| setNewPassword, | ||
| confirmNewPassword, | ||
| setConfirmNewPassword, | ||
| errors, | ||
| }: TPasswordSectionProps) { | ||
| const [showCurrent, setShowCurrent] = useState(false); | ||
| const [showNew, setShowNew] = useState(false); | ||
| const [showConfirm, setShowConfirm] = useState(false); | ||
|
|
||
| return ( | ||
| <div className="bg-white border border-gray-100 rounded-component-lg p-8 shadow-Soft"> | ||
| <header className="mb-7 flex items-start justify-between gap-4"> | ||
| <div> | ||
| <div className="flex gap-4 items-center"> | ||
| <LockIcon /> | ||
| <h2 className="font-heading4 text-text-main font-semibold!"> | ||
| 비밀번호 변경 | ||
| </h2> | ||
| </div> | ||
|
|
||
| <p className="font-body2 text-text-sub mt-2"> | ||
| 안전한 계정 사용을 위해 주기적인 비밀번호 변경을 권장합니다 | ||
| <br /> 영문, 숫자, 특수문자가 모두 포함된 8~16자리의 비밀번호를 | ||
| 설정해 주세요 | ||
| </p> | ||
| </div> | ||
| </header> | ||
| <div className="grid grid-cols-1 tablet:grid-cols-1 gap-x-8 gap-y-6 tablet:max-w-4xl"> | ||
| <div className="tablet:col-span-1"> | ||
| <Input | ||
| type={showCurrent ? "text" : "password"} | ||
| label="현재 비밀번호" | ||
| placeholder="기존 비밀번호를 입력하세요" | ||
| value={currentPassword} | ||
| onChange={(e) => setCurrentPassword(e.target.value)} | ||
| error={!!errors.currentPassword} | ||
| helperText={errors.currentPassword} | ||
| rightElement={ | ||
| <button type="button" onClick={() => setShowCurrent((p) => !p)}> | ||
| {showCurrent ? <EyeOffIcon /> : <EyeIcon />} | ||
| </button> | ||
| } | ||
| /> | ||
| </div> | ||
|
|
||
| <Input | ||
| type={showNew ? "text" : "password"} | ||
| label="새 비밀번호" | ||
| placeholder="8~16자 영문, 숫자, 특수문자" | ||
| value={newPassword} | ||
| onChange={(e) => setNewPassword(e.target.value)} | ||
| error={!!errors.newPassword} | ||
| helperText={errors.newPassword} | ||
| rightElement={ | ||
| <button | ||
| type="button" | ||
| aria-label={ | ||
| showCurrent ? "현재 비밀번호 숨기기" : "현재 비밀번호 보기" | ||
| } | ||
| aria-pressed={showCurrent} | ||
| onClick={() => setShowNew((p) => !p)} | ||
| > | ||
| {showNew ? <EyeOffIcon /> : <EyeIcon />} | ||
| </button> | ||
| } | ||
| /> | ||
| <Input | ||
| type={showConfirm ? "text" : "password"} | ||
| label="새 비밀번호 확인" | ||
| placeholder="한 번 더 입력하세요" | ||
| value={confirmNewPassword} | ||
| onChange={(e) => setConfirmNewPassword(e.target.value)} | ||
| error={!!errors.confirmNewPassword} | ||
| helperText={errors.confirmNewPassword} | ||
| rightElement={ | ||
| <button type="button" onClick={() => setShowConfirm((p) => !p)}> | ||
| {showConfirm ? <EyeOffIcon /> : <EyeIcon />} | ||
| </button> | ||
| } | ||
| /> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,168 @@ | ||
| import type React from "react"; | ||
|
|
||
| import Button from "../common/button/Button"; | ||
| import Input from "../common/input/Input"; | ||
|
|
||
| import CameraIcon from "@/assets/icon/common/camera.svg?react"; | ||
| import CheckIcon from "@/assets/icon/common/check.svg?react"; | ||
| import UserProfileCircleIcon from "@/assets/icon/common/userProfileCircle.svg?react"; | ||
|
|
||
| type TProfileSectionProps = { | ||
| name: string; | ||
| setName: (v: string) => void; | ||
| organizations: { name: string; position: string }[]; | ||
| email: string; | ||
| phoneNumber: string; | ||
| fileRef: React.RefObject<HTMLInputElement | null>; | ||
| preview: string | null; | ||
| openFilePicker: () => void; | ||
| onPickFile: (e: React.ChangeEvent<HTMLInputElement>) => void; | ||
| resetImage: () => void; | ||
| }; | ||
| export default function ProfileSection({ | ||
| name, | ||
| setName, | ||
| organizations, | ||
| email, | ||
| phoneNumber, | ||
| fileRef, | ||
| preview, | ||
| onPickFile, | ||
| openFilePicker, | ||
| resetImage, | ||
| }: TProfileSectionProps) { | ||
| return ( | ||
| <div className="bg-white border border-gray-100 rounded-component-lg p-8 shadow-Soft"> | ||
| <header className="mb-7 flex items-start justify-between gap-4"> | ||
| <div className="flex gap-4 items-center"> | ||
| <UserProfileCircleIcon /> | ||
| <h2 className="font-heading4 text-text-main font-semibold!"> | ||
| 프로필 | ||
| </h2> | ||
| </div> | ||
| </header> | ||
| <div className="flex tablet:flex-row gap-10"> | ||
| <div className="flex flex-col items-center basis-1/4 shrink-0"> | ||
| <div className="w-full text-text-main mb-4 select-none"> | ||
| 프로필 이미지 | ||
| </div> | ||
| <input | ||
| ref={fileRef} | ||
| type="file" | ||
| accept="image/jpeg,image/jpg,image/png,image/webp" | ||
| className="hidden" | ||
| onChange={onPickFile} | ||
| /> | ||
| <div className="relative h-60 w-60 overflow-hidden rounded-full border border-gray-100 bg-gray-100 shadow-sm flex items-center justify-center"> | ||
| {preview ? ( | ||
| <img | ||
| src={preview} | ||
| alt="프로필 이미지 미리보기" | ||
| className="w-full h-full object-cover" | ||
| /> | ||
| ) : ( | ||
| <CameraIcon className="text-text-disabled w-10 h-10" /> | ||
| )} | ||
| </div> | ||
| <div className="mt-5 flex gap-3"> | ||
| <Button | ||
| variant="custom" | ||
| type="button" | ||
| onClick={openFilePicker} | ||
| className="h-7! border border-gray-200 text-text-auth-sub px-4 rounded-component-lg bg-white font-body2 hover:bg-gray-100 transition-colors duration-200 ease-in-out" | ||
| aria-label="프로필 이미지 변경 버튼" | ||
| > | ||
| 변경 | ||
| </Button> | ||
| <Button | ||
| variant="custom" | ||
| type="button" | ||
| onClick={resetImage} | ||
| className="h-7! border border-gray-200 text-text-auth-sub px-4 rounded-component-lg bg-white font-body2 hover:bg-gray-100 transition-colors duration-200 ease-in-out disabled:cursor-not-allowed disabled:opacity-40" | ||
| aria-label="프로필 이미지 초기화 버튼" | ||
| disabled={!preview} | ||
| > | ||
| 초기화 | ||
| </Button> | ||
| </div> | ||
| </div> | ||
| <div className="grid grid-cols-1 tablet:grid-cols-2 gap-x-6 basis-3/4 gap-y-5 w-full"> | ||
| <div className="col-span-2"> | ||
| <Input | ||
| label="이름" | ||
| value={name} | ||
| onChange={(e) => setName(e.target.value)} | ||
| /> | ||
| </div> | ||
| <div className="col-span-2"> | ||
| <div className="text-text-main mb-2 ml-1">소속 조직</div> | ||
| {organizations.length === 0 ? ( | ||
| <div className="text-text-disabled font-body2 ml-1"> | ||
| 소속된 조직이 없습니다. | ||
| </div> | ||
| ) : ( | ||
| <> | ||
| <div className="flex gap-4 px-1 mb-1 text-text-sub"> | ||
| <div className="flex-1 font-body2">조직</div> | ||
| <div className="flex-1 font-body2">직책</div> | ||
| </div> | ||
| <div className="group relative flex flex-col gap-4"> | ||
| {organizations.map((org, idx) => ( | ||
| <div key={idx} className="flex gap-4 "> | ||
| <div className="flex-1"> | ||
| <Input | ||
| value={org.name} | ||
| disabled | ||
| inputClassName="text-text-main" | ||
| containerClassName="bg-gray-100" | ||
| readOnly | ||
| /> | ||
| </div> | ||
| <div className="flex-1"> | ||
| <Input | ||
| value={org.position} | ||
| disabled | ||
| inputClassName="text-text-sub" | ||
| containerClassName="bg-gray-100" | ||
| readOnly | ||
| /> | ||
| </div> | ||
| </div> | ||
| ))} | ||
|
|
||
| <div className="pointer-events-none absolute top-full mt-1 whitespace-nowrap rounded bg-gray-800 px-2 py-1 text-xs text-white opacity-0 transition-opacity group-hover:opacity-100"> | ||
| 조직 정보는 별도 조직페이지에서 수정할 수 있습니다. | ||
| </div> | ||
| </div> | ||
| </> | ||
| )} | ||
| </div> | ||
| <div className="col-span-2 group relative w-full"> | ||
| <Input | ||
| label="이메일" | ||
| value={email} | ||
| disabled={true} | ||
| rightElement={<CheckIcon className="w-6 h-6 text-chart-3" />} | ||
| readOnly | ||
| /> | ||
| <div className="pointer-events-none absolute top-full mt-1 whitespace-nowrap rounded bg-gray-800 px-2 py-1 text-xs text-white opacity-0 transition-opacity group-hover:opacity-100"> | ||
| 이메일은 변경할 수 없습니다. | ||
| </div> | ||
| </div> | ||
| <div className="col-span-2 group relative w-full"> | ||
| <Input | ||
| label="전화번호" | ||
| value={phoneNumber} | ||
| disabled={true} | ||
| rightElement={<CheckIcon className="w-6 h-6 text-chart-3" />} | ||
| readOnly | ||
| /> | ||
| <div className="pointer-events-none absolute top-full mt-1 whitespace-nowrap rounded bg-gray-800 px-2 py-1 text-xs text-white opacity-0 transition-opacity group-hover:opacity-100"> | ||
| 전화번호는 변경할 수 없습니다. | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import type { ChangeEvent } from "react"; | ||
| import { useEffect, useRef, useState } from "react"; | ||
|
|
||
| export function useImageUploader() { | ||
| const fileRef = useRef<HTMLInputElement | null>(null); | ||
| const [file, setFile] = useState<File | null>(null); | ||
| const [preview, setPreview] = useState<string | null>(null); | ||
|
|
||
| const openFilePicker = () => { | ||
| fileRef.current?.click(); | ||
| }; | ||
| const onPickFile = (e: ChangeEvent<HTMLInputElement>) => { | ||
| const f = e.target.files?.[0]; | ||
| if (!f) return; | ||
|
|
||
| setFile(f); | ||
|
|
||
| const url = URL.createObjectURL(f); | ||
| setPreview(url); | ||
| }; | ||
| const resetImage = () => { | ||
| if (fileRef.current) { | ||
| fileRef.current.value = ""; | ||
| } | ||
| setFile(null); | ||
| setPreview(null); | ||
| }; | ||
|
jjjsun marked this conversation as resolved.
Resolved
jjjsun marked this conversation as resolved.
Resolved
|
||
| useEffect(() => { | ||
| return () => { | ||
| if (preview) URL.revokeObjectURL(preview); | ||
| }; | ||
| }); | ||
| return { fileRef, file, preview, openFilePicker, onPickFile, resetImage }; | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.