Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion components/Modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ const Modal = ({
/>

<div
className={`${width} relative max-h-[90vh] overflow-y-auto rounded-lg bg-background shadow-custom`}
className={`${width} relative max-h-[90vh] overflow-y-auto rounded-lg bg-background shadow-custom mo:mx-4`}
>
{/* 닫기 버튼 영역 */}
<button
Expand Down
9 changes: 4 additions & 5 deletions components/Modal/WikiQuizModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import Modal from './Modal';
export const QUIZ_MESSAGES = {
ERROR: '잘못된 답변입니다. 다시 시도해주세요.',
SUBMIT_ERROR: '퀴즈 제출 중 오류가 발생했습니다.',
WIKI_DESCRIPTION: '위키드는 친구들과 함께 공유하는 즐거운 공간입니다.',
WIKI_GUIDANCE: '작성 시 서로를 배려하고 존중해주세요.',
WIKI_DESCRIPTION: '위키드는 지인들과 함께하는 즐거운 공간입니다.',
WIKI_GUIDANCE: '지인에게 상처를 주지 않도록 작성해 주세요.',
} as const;

function WikiQuizModal({
Expand Down Expand Up @@ -83,9 +83,9 @@ function WikiQuizModal({
/>
</div>
<p className="text-center text-14 text-gray-400">
위키에 작성하기 위해
다음 퀴즈를 맞추고
<br className={keyboardVisible ? 'mo:hidden' : ''} />
아래 퀴즈에 답해주세요.
위키를 작성해 보세요.
</p>
</div>

Expand Down Expand Up @@ -121,7 +121,6 @@ function WikiQuizModal({
제출
</Button>
</form>

{!keyboardVisible && (
<div className="mt-2 px-4 text-center text-12 text-gray-400">
<p>{QUIZ_MESSAGES.WIKI_DESCRIPTION}</p>
Expand Down
156 changes: 73 additions & 83 deletions components/UserProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ function UserProfile({

// 프로필 이미지 관련 상태와 이벤트 핸들러 훅
const {
isLoading, // 이미지 로딩 상태
previewImage, // 이미지 미리보기 URL
fileInputRef, // 파일 입력 요소 참조
handleImageClick, // 이미지 클릭 핸들러
handleFileChange, // 파일 변경 핸들러
isLoading,
previewImage,
fileInputRef,
handleImageClick,
handleFileChange,
} = useProfileImage((url) => onDataChange('image', url));

/**
Expand All @@ -52,24 +52,26 @@ function UserProfile({
}

return (
<p className="flex h-[18px] w-[200px] text-14 mo:w-[180px] mo:text-12 pc:gap-[20px]">
<p className="flex w-[200px] text-14 mo:h-[18px] mo:w-[180px] mo:text-12 ta:h-[24px] pc:h-[24px]">
<span className="flex-[1] text-gray-400">{label}</span>
<span className="flex-[2] truncate text-gray-500">{data[field]}</span>
{/* truncate 클래스 - overflow:hidden, text-overflow:ellipsis, white-space:nowrap */}
<span className={`flex-[2] truncate text-gray-500`}>{data[field]}</span>
</p>
);
};

return (
// 메인 컨테이너
<div className="bgCard max-w-4xl rounded-custom bg-background shadow-custom dark:shadow-custom-dark pc:h-auto pc:w-[320px] pc:p-7 tamo:w-full tamo:p-5">
{/* 레이아웃 컨테이너: PC에서는 세로, 모바일/태블릿에서는 가로 배치 */}
<div
className={`${isEditing ? 'mo:px-[37px] mo:pb-[17px] mo:pl-[34px] mo:pt-[15px] ta:px-[16px] ta:pb-[37px] ta:pt-[20px] pc:h-auto pc:pb-[36px] pc:pl-[40px]' : 'pc:h-[671px]'}flex max-w-4xl flex-col rounded-custom bg-background shadow-custom dark:shadow-custom-dark mo:pl-[20px] mo:pt-[15px] ta:px-[30px] ta:pb-[5px] ta:pt-[20px] pc:w-[320px] pc:px-[30px] pc:pb-[47px] pc:pt-[60px] tamo:w-full`}
>
{/* 프로필 이미지와 정보를 포함하는 상단 컨테이너 */}
<div
className={`flex ${isEditing ? 'flex-col' : 'flex-col mo:flex-row ta:flex-row pc:flex-col'}`}
>
{/* 프로필 이미지 섹션 */}
<div
className={`flex items-center justify-center pc:pb-[60px] pc:pt-[40px] tamo:pl-[10px] tamo:pt-4 ${isEditing ? '' : 'mo:self-start ta:self-start'}`}
className={`flex items-center justify-center mo:pr-[10px] ta:pr-[20px] ${
isEditing ? 'mo:p-0 ta:p-0 tamo:justify-center' : 'tamo:self-start'
}`}
>
{/* 이미지 업로드 버튼 */}
<button
Expand All @@ -80,10 +82,8 @@ function UserProfile({
>
{/* 이미지 컨테이너 */}
<div
className={`relative ${
isEditing
? 'mo:size-[62px] ta:size-[71px] pc:size-48'
: 'mo:size-[62px] ta:size-[71px] pc:size-48'
className={`relative mo:size-[62px] ta:size-[71px] pc:size-[200px] ${
isEditing ? 'mo:size-[62px] ta:size-[71px] pc:size-[200px]' : ''
}`}
>
{/* 프로필 이미지 */}
Expand Down Expand Up @@ -118,7 +118,7 @@ function UserProfile({
</div>
)}
</button>
{/* 숨겨진 파일 입력 필드: 실제 파일 선택 다이얼로그를 위한 요소 */}
{/* 숨겨진 파일 입력 필드 */}
<input
ref={fileInputRef}
type="file"
Expand All @@ -130,94 +130,84 @@ function UserProfile({
/>
</div>

{/* 프로필 정보 섹션: 사용자 정보를 표시하는 영역 */}
{/* 프로필 정보 섹션 */}
<div
className={`${isEditing ? 'mt-4 mo:justify-center' : 'mo:ml-4 mo:flex-1 mo:pl-[20px] ta:ml-4 ta:flex-1 ta:pl-[40px] pc:mt-4'} mt-6 mo:pc:w-full`}
className={`${
isEditing
? 'mo:pt-[24px] ta:pb-[37px] ta:pt-[34px] tamo:justify-center'
: 'mo:flex-1 mo:pl-[10px] ta:flex-1 ta:pl-[20px]'
} pc:pt-[60px] mo:pc:w-full`}
>
<div className="space-y-3">
{/* 기본 정보 영역: 항상 표시되는 필수 정보들 */}
<div
className={`${isEditing ? 'space-y-[16px]' : 'mo:space-y-[8px] ta:space-y-[4px]'}`}
>
{/* 기본 정보 영역 */}
<div
className={`${
isEditing
? 'mo:space-y-3 ta:grid ta:grid-cols-2 ta:place-items-center ta:gap-3 ta:space-y-0 pc:space-y-3'
: ''
} space-y-3`}
? 'mo:space-y-[16px] ta:grid ta:grid-cols-2 ta:gap-[16px] ta:space-y-0 pc:space-y-[16px]'
: 'mo:space-y-[8px] ta:space-y-[4px] pc:space-y-[16px]'
}`}
>
{renderField('거주 도시', 'city')}
{renderField('MBTI', 'mbti')}
{renderField('직업', 'job')}
{isEditing && renderField('SNS 계정', 'sns')}
</div>

{/* 추가 정보 영역: 편집 모드와 조회 모드에서 다르게 표시 */}
{isEditing ? (
// 편집 모드: 모든 필드를 그리드로 표시
<div className="mo:space-y-3 ta:grid ta:grid-cols-2 ta:place-items-center ta:gap-3 pc:space-y-3">
{/* PC용 추가 정보 섹션 */}
{!isEditing && (
<div className="hidden pc:block pc:space-y-[16px] pc:pt-[16px]">
{renderField('SNS 계정', 'sns')}
{renderField('생일', 'birthday')}
{renderField('별명', 'nickname')}
{renderField('혈액형', 'bloodType')}
{renderField('국적', 'nationality')}
</div>
)}

{/* 편집 모드일 때 추가 정보 */}
{isEditing && (
<div className="mo:space-y-[16px] ta:grid ta:grid-cols-2 ta:gap-[16px] pc:space-y-[16px]">
{renderField('생일', 'birthday')}
{renderField('별명', 'nickname')}
{renderField('혈액형', 'bloodType')}
{renderField('국적', 'nationality')}
</div>
) : (
// 조회 모드: PC와 모바일/태블릿에서 다르게 표시
<>
{/* 모바일/태블릿용 펼침/접힘 섹션 */}
<div className="pc:hidden">
{!isExpanded ? (
// 접힌 상태: 펼치기 버튼 표시
<div className="flex justify-center">
<button
onClick={toggleExpand}
className="flex items-center gap-2 text-14md text-green-200 hover:text-green-300"
>
<Image
src="/icon/icon-expand.svg"
alt="Expand"
width={16}
height={16}
/>
</button>
</div>
) : (
// 펼친 상태: 추가 정보와 접기 버튼 표시
<div>
<div className="space-y-3">
{renderField('SNS 계정', 'sns')}
{renderField('생일', 'birthday')}
{renderField('별명', 'nickname')}
{renderField('혈액형', 'bloodType')}
{renderField('국적', 'nationality')}
</div>
<div className="mt-3 flex justify-center">
<button
onClick={toggleExpand}
className="flex items-center gap-2 text-14md text-green-200 hover:text-green-300"
>
<Image
src="/icon/icon-collapse.svg"
alt="Collapse"
width={16}
height={16}
/>
</button>
</div>
</div>
)}
</div>
)}

{/* PC용 추가 정보 섹션: 항상 표시 */}
<div className="hidden space-y-3 pc:block">
{renderField('SNS 계정', 'sns')}
{renderField('생일', 'birthday')}
{renderField('별명', 'nickname')}
{renderField('혈액형', 'bloodType')}
{renderField('국적', 'nationality')}
</div>
</>
{/* 모바일/태블릿용 추가 정보 섹션 */}
{!isEditing && isExpanded && (
<div className="mo:space-y-[8px] ta:space-y-[4px] pc:hidden">
{renderField('SNS 계정', 'sns')}
{renderField('생일', 'birthday')}
{renderField('별명', 'nickname')}
{renderField('혈액형', 'bloodType')}
{renderField('국적', 'nationality')}
</div>
)}
</div>
</div>
</div>

{/* 모바일/태블릿용 확장 버튼 */}
{!isEditing && (
<div className="flex justify-center pc:hidden tamo:py-[5px]">
<button
onClick={toggleExpand}
className="flex items-center gap-2 text-14md text-green-200 hover:text-green-300"
>
<Image
src={
isExpanded ? '/icon/icon-collapse.svg' : '/icon/icon-expand.svg'
}
alt={isExpanded ? 'Collapse' : 'Expand'}
width={24}
height={24}
/>
</button>
</div>
)}
</div>
);
}
Expand Down
57 changes: 46 additions & 11 deletions hooks/modal/useKeyboardVisibilty.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,35 @@ export function useKeyboardVisibility(
isMobile: boolean
) {
const [keyboardVisible, setKeyboardVisible] = useState(false);
const [initialScreenHeight] = useState(
typeof window !== 'undefined' ? window.screen.height : 0
);

useEffect(() => {
if (!isMobile) return undefined;
if (!isMobile) {
setKeyboardVisible(false);
return undefined;
}

let scrollTimeout: NodeJS.Timeout;

const handleKeyboardVisibility = (): void => {
const isKeyboardVisible = window.innerHeight < window.screen.height;
setKeyboardVisible(isKeyboardVisible);
// visualViewport API 사용 (지원되는 경우)
if (window.visualViewport) {
const isKeyboardVisible =
window.visualViewport.height < initialScreenHeight * 0.8;
setKeyboardVisible(isKeyboardVisible);
} else {
// fallback: 기존 방식
const isKeyboardVisible =
window.innerHeight < initialScreenHeight * 0.8;
setKeyboardVisible(isKeyboardVisible);
}

// 키보드가 보일 때 입력 필드로 포커스 및 스크롤
if (isKeyboardVisible && inputRef.current !== null) {
inputRef.current.focus();
void setTimeout(() => {
if (inputRef.current !== null) {
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(() => {
if (inputRef.current !== null) {
inputRef.current.scrollIntoView({
behavior: 'smooth',
Expand All @@ -27,12 +44,30 @@ export function useKeyboardVisibility(
}
};

// 초기 상태 설정
handleKeyboardVisibility();
window.addEventListener('resize', handleKeyboardVisibility);
return () => {
window.removeEventListener('resize', handleKeyboardVisibility);
};
}, [isMobile, inputRef]);

// 이벤트 리스너 설정
if (window.visualViewport) {
window.visualViewport.addEventListener(
'resize',
handleKeyboardVisibility
);
return () => {
window.visualViewport?.removeEventListener(
'resize',
handleKeyboardVisibility
);
clearTimeout(scrollTimeout);
};
} else {
window.addEventListener('resize', handleKeyboardVisibility);
return () => {
window.removeEventListener('resize', handleKeyboardVisibility);
clearTimeout(scrollTimeout);
};
}
}, [isMobile, inputRef, initialScreenHeight]);

return keyboardVisible;
}
Loading