Skip to content
48 changes: 47 additions & 1 deletion package-lock.json

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

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@hookform/resolvers": "^5.0.1",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
Expand All @@ -11,11 +12,13 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-helmet": "^6.1.0",
"react-hook-form": "^7.56.3",
"react-icons": "5.3.0",
"react-router-dom": "^7.2.0",
"react-scripts": "5.0.1",
"typescript": "^4.9.5",
"web-vitals": "^4.2.4"
"web-vitals": "^4.2.4",
"zod": "^3.24.4"
},
"scripts": {
"start": "react-scripts start",
Expand Down
2 changes: 1 addition & 1 deletion src/api/client/interceptors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { InternalAxiosRequestConfig, AxiosHeaders } from 'axios';
export const requestInterceptor = (
config: InternalAxiosRequestConfig
): InternalAxiosRequestConfig => {
const tokenString = localStorage.getItem('token');
const tokenString = localStorage.getItem('access_token');
let token = '';

if (tokenString) {
Expand Down
59 changes: 59 additions & 0 deletions src/api/services/auth.services.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { SignInFormData } from '../../pages/LoginPage';
import { SignUpFormData } from '../../pages/SignUpPage';
import { User } from '../../types/types';
import requestor from '../client/requestor';

interface AuthResponse {
accessToken: string;
refreshToken: string;
user: User;
}

class AuthService {
async signUp(data: SignUpFormData): Promise<AuthResponse> {
try {
const response = await requestor.post<AuthResponse>('/auth/signUp', data);
return response.data;
} catch (error: any) {
console.error('회원가입 실패:', error);
if (error.response && error.response.data) {
throw error.response.data;
}
throw error;
}
}

async login(data: SignInFormData) {
try {
const response = await requestor.post<AuthResponse>('/auth/signIn', data);
localStorage.setItem(
'access_token',
JSON.stringify({ token: response.data.accessToken })
);
localStorage.setItem(
'refresh_token',
JSON.stringify({ token: response.data.refreshToken })
);
localStorage.setItem('user', JSON.stringify(response.data.user));

return response.data;
} catch (error: any) {
console.error('로그인 실패:', error);
if (error.response && error.response.data) {
throw error.response.data;
}
throw error;
}
}

logout() {
// 로컬 스토리지에서 인증 관련 데이터 제거
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
localStorage.removeItem('user');
}
}

const authService = new AuthService();

export default authService;
15 changes: 8 additions & 7 deletions src/components/common/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import './Button.css';
import { MouseEvent, ReactNode } from 'react';
import { ButtonHTMLAttributes, MouseEvent, PropsWithChildren } from 'react';

interface ButtonProps {
children?: ReactNode;
onClick: (e: MouseEvent<HTMLButtonElement>) => void;
disabled?: boolean;
}
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {}

function Button({ children, onClick, disabled, ...rest }: ButtonProps) {
function Button({
children,
onClick,
disabled,
...rest
}: PropsWithChildren<ButtonProps>) {
return (
<button className="button" onClick={onClick} disabled={disabled} {...rest}>
{children}
Expand Down
8 changes: 3 additions & 5 deletions src/components/common/ButtonMedium.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import './ButtonMedium.css';
import { MouseEvent, ReactNode } from 'react';
import { ButtonHTMLAttributes, PropsWithChildren } from 'react';

interface ButtonMediumProps {
children: ReactNode;
onClick: (e: MouseEvent<HTMLButtonElement>) => void;
interface ButtonMediumProps extends ButtonHTMLAttributes<HTMLButtonElement> {
imgSrc: string;
imgAlt: string;
}
Expand All @@ -14,7 +12,7 @@ function ButtonMedium({
imgSrc,
imgAlt,
...rest
}: ButtonMediumProps) {
}: PropsWithChildren<ButtonMediumProps>) {
return (
<div className="ButtonMediumContainer">
<button className="ButtonMedium" onClick={onClick}>
Expand Down
1 change: 1 addition & 0 deletions src/components/common/Dropdown.css
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
font-size: 16px;
font-weight: 400;
color: var(--gray-500);
white-space: nowrap;
}

.commentDropdown li {
Expand Down
22 changes: 13 additions & 9 deletions src/components/common/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,44 @@
import { useEffect, useRef } from 'react';
import { MouseEventHandler, useEffect, useRef } from 'react';
import './Dropdown.css';

interface DropdownItem {
label: string;
onClick: () => void;
onClick: MouseEventHandler;
}

interface DropdownProps {
items: DropdownItem[];
isOpen: boolean;
onClose: () => void;
triggerElementId?: string;
}

function Dropdown({ items, isOpen, onClose }: DropdownProps) {
console.log(isOpen);
function Dropdown({ items, isOpen, onClose, triggerElementId }: DropdownProps) {
const dropdownRef = useRef<HTMLUListElement>(null);

useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as Element;

// 트리거 요소(이미지)를 클릭한 경우 무시
if (triggerElementId && target.id === triggerElementId) {
return;
}

if (
isOpen &&
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
onClose();
}
};

if (isOpen) {
window.addEventListener('mousedown', handleClickOutside);
}
window.addEventListener('mousedown', handleClickOutside);

return () => {
window.removeEventListener('mousedown', handleClickOutside);
};
}, [isOpen, onClose]);
}, [onClose]);

return (
<>
Expand Down
2 changes: 1 addition & 1 deletion src/components/common/FileInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import './FileInput.css';
import PlusIcon from '../../assets/icons/plus-icon.svg';

interface FileInputProps {
value: string | null | File | null;
value: string | File | null;
onChange: (name: string, value: File | null) => void;
}

Expand Down
8 changes: 8 additions & 0 deletions src/components/common/Input.css
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,11 @@
.inputContainer input:focus {
outline-color: var(--blue);
}

.error-message {
position: absolute;
bottom: -53px;
color: red;
font-size: 14px;
margin-left: 5px;
}
45 changes: 22 additions & 23 deletions src/components/common/Input.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,29 @@
import { ChangeEvent, FocusEventHandler, Ref } from 'react';
import { InputHTMLAttributes, forwardRef } from 'react';
import './Input.css';

interface InputProps {
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
label: string;
id: string;
value: string;
type: string;
placeholder?: string;
ref?: Ref<HTMLInputElement>;
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
onBlur?: FocusEventHandler<HTMLInputElement>;
error?: string;
}

function Input({ label, id, value, onChange, onBlur, ...rest }: InputProps) {
return (
<div className="inputContainer">
<label>{label}</label>
<input
id={id}
value={value}
onChange={onChange}
onBlur={onBlur}
{...rest}
/>
</div>
);
}
const Input = forwardRef<HTMLInputElement, InputProps>(
({ label, id, value, onChange, onBlur, error, ...rest }, ref) => {
return (
<div className="inputContainer">
<label htmlFor={id}>{label}</label>
<input
id={id}
value={value}
onChange={onChange}
onBlur={onBlur}
ref={ref}
className={error ? 'error' : ''}
{...rest}
/>
{error && <p className="error-message">{error}</p>}
</div>
);
}
);

export default Input;
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.ButtonLarge {
.LinkButton {
display: flex;
align-items: center;
justify-content: center;
Expand Down
Loading
Loading