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
11 changes: 11 additions & 0 deletions next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',

images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'sprint-fe-project.s3.ap-northeast-2.amazonaws.com',
port: '',
pathname: '/**',
},
Comment on lines +5 to +12

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 코드를 추가 하신 이유가 무엇인가요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

외부 이미지를 사용하기 위해 추가한 설정입니다.
Next.js의 Image 컴포넌트는 보안상의 이유로 외부 도메인의 이미지를 기본적으로 차단합니다.

받아온 이미지 URL을 보면

"imageUrl": "https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/taskify/task_image/7-6_50923_1749446154135.jpeg",

이렇고, 외부 API에서 데이터를 받아올때. 이미지 URL은 AWS S3에 저장해서 제공하고 있습니다.(amazonaws)

Next.js에서 이 이미지를 표시하려면 S3 도메인을 허용해야 했습니다.

images: {
  remotePatterns: [
    {
      protocol: 'https',        // HTTPS만 허용
      hostname: 'sprint-fe-project.s3.ap-northeast-2.amazonaws.com', // AWS S3 버킷
      port: '',                 // 기본 포트 (443)
      pathname: '/**',          // 모든 경로 허용
    },
  ],
},

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

답변 감사합니다!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

외부 도메인의 이미지를 차단하는 지는 처음 알았네용 😮

],
},
}

export default nextConfig
3 changes: 3 additions & 0 deletions public/images/calendar.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions public/images/config.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions public/images/plus.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file removed src/app/api/.gitkeep
Empty file.
18 changes: 18 additions & 0 deletions src/app/api/axiosClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import axios from 'axios'

const axiosClient = axios.create({
baseURL: 'https://sp-taskify-api.vercel.app/7-6',
})

// 작업용 임시 토큰
const TEMP_TOKEN = process.env.NEXT_PUBLIC_API_TOKEN

axiosClient.interceptors.request.use((config) => {
if (TEMP_TOKEN) {
config.headers['Authorization'] = `Bearer ${TEMP_TOKEN}`
}

return config
})
Comment on lines +10 to +16

This comment was marked as resolved.


export default axiosClient
46 changes: 46 additions & 0 deletions src/app/api/useCards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//size일단 10으로 하고, 나중에 커서아이디 받아서 무한 스크롤 구현해야 함.
import { useQuery } from '@tanstack/react-query'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tanStack Query 사용하셨군용!!


import axiosClient from './axiosClient'

export interface Assignee {
id: number
nickname: string
profileImageUrl: string | null
}
export interface Card {
id: number
title: string
description: string
tags: string[]
dueDate: string
assignee: Assignee
imageUrl: string
teamId: string
dashboardId: number
columnId: number
createdAt: string
updatedAt: string
}
export interface CardResponse {
cards: Card[]
totalCount: number
cursorId: number
}

export async function fetchCards(
columnId: number,
size: number = 10,
): Promise<CardResponse> {
const res = await axiosClient.get<CardResponse>(
`/cards?size=${size}&columnId=${columnId}`,
)
return res.data
}

export default function useCards(columnId: number) {
return useQuery<CardResponse>({
queryKey: ['columnId', columnId],
queryFn: () => fetchCards(columnId),
})
}
32 changes: 32 additions & 0 deletions src/app/api/useColumns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useQuery } from '@tanstack/react-query'

import axiosClient from './axiosClient'

//타입
export interface Column {
id: number
title: string
teamId: string
dashboardId: number
createdAt: string
updatedAt: string
}
export interface ColumnsResponse {
data: Column[]
}

//fetch 함수 (API 호출 전용)
export async function fetchColumns(dashboardId: number): Promise<Column[]> {
const res = await axiosClient.get<ColumnsResponse>(
`/columns?dashboardId=${dashboardId}`,
)
return res.data.data
}

//useQuery
export default function useColumns(dashboardId: number) {
return useQuery<Column[]>({
queryKey: ['columns', dashboardId],
queryFn: () => fetchColumns(dashboardId),
})
Comment on lines +27 to +31
Copy link

@Insung-Jo Insung-Jo Jun 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아직 useQuery에 대해 조사하지 못해서 질문드립니다!
useQuery는 보통 어떤 상황에서 사용하나요?

Copy link
Contributor Author

@dkslel1225 dkslel1225 Jun 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useQuery 를 사용하면 기존 방식대로 useEffect, useState를 사용해서 데이터를 호출/처리할 필요가 없어지는데요

복잡한 서버 상태 관리를 간결하게 해주는 훅이라서,
실제 API 호출 함수(fetch)와 <->데이터를 받고 상태 관리하려고 하는 컴포넌트 사이의 중간 처리 장치쯤의.. 느낌입니다.

useQuery에 API 호출 함수를 연결하고 몇가지 설정하면, 호출 시 복잡한 상태 관리는 useQuery가 자동으로 처리해줍니다.
컴포넌트에서는 작성해둔 커스텀 훅을 호출해서 useQuery가 제공하는 다양한 상태를 활용하면 됩니다.

비포 애프터 예시)

// 기존 방식 (복잡함)
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)

useEffect(() => {
  fetchData()
    .then(setData)
    .catch(setError) //-->error
    .finally(() => setLoading(false)) //-->isLoading
}, [])

// useQuery (간단함)
const { data, isLoading, error } = useQuery({
  queryKey: ['data'], // useEffect, setData가 따로 필요 없음
  queryFn: fetchData
})
if (isLoading) return <p>loading...</p>
if (error) return <p>error...{error.message}</p>

//제 코드에서는 {
//  queryKey: ['data'], 
//  queryFn: fetchData
// }) 이부분을 커스텀 훅에 따로 넣고, 컴포넌트에서는 해당 훅(useColumns)를 호출한 구조입니다.

로딩/에러상태 관리, 캐싱, 리페칭, 의존성 변화 감지, 실패시 자동 재시도 등 복잡한 로직을 알아서 처리해주는 부분이 많습니다.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 useQuery 사용이 궁금했는데 코드로 보니 이해가 잘 되네요!!

}
Empty file removed src/app/dashboard/.gitkeep
Empty file.
28 changes: 28 additions & 0 deletions src/app/dashboard/[id]/Card/Card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Image from 'next/image'

import type { Card } from '@/app/api/useCards'

import Tags from './Tags'

export default function Card({ card }: { card: Card }) {
const { imageUrl, title, tags, dueDate, assignee } = card
return (
<div className="BG-white Border-section relative w-314 rounded-6 border-solid px-20 py-16">
Todo Card
{imageUrl && (
<Image
src={imageUrl}
alt="카드 이미지"
width={400}
height={600}
className="h-auto w-full rounded-6 object-contain"
priority
/>
)}
<p>{title}</p>
<Tags tags={tags} />
<p>{dueDate}</p>
<p>프로필</p>
</div>
)
}
17 changes: 17 additions & 0 deletions src/app/dashboard/[id]/Card/Tags.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export default function Tags({ tags }: { tags: string[] }) {
//태그 컬러 - 랜덤 배정
//카드 생성 시 - 동일 태그 입력 불가하도록
return (
<div className="flex gap-6">
{tags.map((tag) => (
<span
key={tag}
className="inline-block whitespace-nowrap rounded-4 px-9 pb-3 pt-5"
style={{ backgroundColor: '#F7DBF0', color: '#D549B6' }}
>
{tag}
</span>
))}
</div>
)
}
44 changes: 44 additions & 0 deletions src/app/dashboard/[id]/Column/Column.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import Image from 'next/image'

import useCards from '@/app/api/useCards'
import type { Column } from '@/app/api/useColumns'

import Card from '../Card/Card'
export default function Column({ column }: { column: Column }) {
const { id, title }: { id: number; title: string } = column
const { data, isLoading, error } = useCards(id)

if (isLoading) return <p>loading...</p>
if (error) return <p>error...{error.message}</p>

return (
<div className="BG-gray Border-column flex w-354 shrink-0 flex-col gap-16 p-20">
<div className="mb-24 flex items-center justify-between">
<div className="flex items-center">
<div className="mb-7 mr-8 size-8 rounded-25 bg-blue-500"></div>
<h2 className="mr-12 text-18 font-bold leading-none">{title}</h2>
<span className="Text-gray block size-20 rounded-4 bg-[#EEEEEE] pt-3 text-center text-12 font-medium leading-tight dark:bg-[#2E2E2E]">
{data?.totalCount}
</span>
</div>
<Image
src={'/images/config.svg'}
alt="컬럼 설정"
width={20}
height={20}
/>
</div>
<button className="BG-white Border-btn rounded-6 px-146 py-9">
<div className="flex h-22 w-22 items-center justify-center rounded-4 bg-blue-100">
<Image
src={'/images/plus.svg'}
alt="추가하기"
width={10}
height={10}
/>
</div>
</button>
{data?.cards.map((card) => <Card key={card.id} card={card} />)}
</div>
)
}
37 changes: 37 additions & 0 deletions src/app/dashboard/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use client'

import Image from 'next/image'

import useColumns from '@/app/api/useColumns'

import Column from './Column/Column'
export default function DashboardID() {
const dashboard = 15120

This comment was marked as resolved.

const { data: columns, isLoading, error } = useColumns(dashboard)
if (isLoading) return <p>loading...</p>
if (error) return <p>error...{error.message}</p>

return (
<>
<div className="fixed left-0 h-1080 w-300 bg-gray-100">사이드바</div>
<div className="ml-300">
<div className="flex">
{columns?.map((column) => <Column key={column.id} column={column} />)}
<div className="BG-gray Border-column p-20">
<button className="BG-white Border-btn flex items-center gap-12 whitespace-nowrap rounded-8 px-85 pb-20 pt-24 text-18 font-bold">
<span>새로운 컬럼 추가하기</span>
<div className="flex h-22 w-22 items-center justify-center rounded-4 bg-blue-100">
<Image
src={'/images/plus.svg'}
alt="플러스 아이콘"
width={10}
height={10}
/>
</div>
</button>
</div>
</div>
</div>
</>
)
}
3 changes: 3 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ body {
.Border-section {
@apply border border-[#D9D9D9] dark:border-[#000000];
}
.Border-column {
@apply border-r border-[#EEEEEE] dark:border-[#262626];
}
.BG-addPhoto {
@apply bg-[#F5F5F5] dark:bg-[#2E2E2E];
}
Expand Down
1 change: 1 addition & 0 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import './globals.css'
import type { Metadata } from 'next'

import { Providers } from './providers'
import ThemeToggle from './shared/components/ThemeToggle'

export const metadata: Metadata = {
title: 'Coplan',
Expand Down
17 changes: 13 additions & 4 deletions src/app/providers.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
'use client'

import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ThemeProvider } from 'next-themes'
import { ReactNode } from 'react'
import { ReactNode, useState } from 'react'

export function Providers({ children }: { children: ReactNode }) {
const [queryClient] = useState(() => new QueryClient())

return (
<ThemeProvider attribute="class" defaultTheme="system" enableSystem={true}>
{children}
</ThemeProvider>
<QueryClientProvider client={queryClient}>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tanStack Query의 경우도 provider로 감싸줘야 하는군용

<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem={true}
>
{children}
</ThemeProvider>
</QueryClientProvider>
)
}