Skip to content

Commit

Permalink
Merge pull request #19 from youngwan2/feature/views
Browse files Browse the repository at this point in the history
feature/실시간 인기 명언 조회 기능 추가
  • Loading branch information
youngwan2 authored Mar 14, 2024
2 parents 38ed282 + b58ff40 commit d776dfc
Show file tree
Hide file tree
Showing 13 changed files with 194 additions and 22 deletions.
6 changes: 4 additions & 2 deletions src/app/(quotes)/quotes/[category]/[name]/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export default async function DetailPage({
`
const result = await db.query(query, [id, decodeURIComponent(name)])
const item = result.rows[0]

return item
} catch (error) {
console.error(
Expand All @@ -36,8 +37,9 @@ export default async function DetailPage({
}

const item = await getQuoteDetail(id)

if (!item) return <ReplaceMessageCard childern="데이터를 조회중 입니다.." />


return (
<article className="sm:p-[4em] p-[1em] min-h-[100vh] h-full mx-auto my-[3em] perspective-500 flex flex-col max-w-[1300px] bg-[#0000001b]">
<h2 className="sm:text-[1.5em] text-[1.2em] flex justify-center items-center p-[10px] text-center text-white">
Expand All @@ -56,7 +58,7 @@ export default async function DetailPage({
<p className="">{item.quote}</p>
<QuoteLikeBox id={id} />
</blockquote>

<div className="flex">
{/* 컨트롤 버튼 */}
<DetailPageControlButtons item={item} />
Expand Down
41 changes: 41 additions & 0 deletions src/app/(quotes)/quotes/populars/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use client'
import ReplaceMessageCard from "@/components/UI/common/ReplaceMessageCard"
import PopularQuoteList from "@/components/UI/quote/PopularQuoteList"
import { useSwrFetch } from "@/utils/swr"
import toast from "react-hot-toast"
import { HiRefresh } from "react-icons/hi"

export interface QuoteType {
quote_id: number
quote: string
author: string
job: string
category: string
views: number
}
export default function PopularQuotesPage() {

const url = '/api/quotes/populars'
const { data, mutate } = useSwrFetch(url,1000 * 60, false)
const { quotes } = data || { quotes: null }

async function reload() {
toast.promise(mutate(), {
loading:'새로운 데이터를 불러오는 중입니다.',
success:'최신 데이터로 갱신되었습니다.',
error:'서버 문제로 데이터 조회에 실패하였습니다.'
})
}

if (!quotes) return <ReplaceMessageCard childern='데이터를 조회중 입니다..' />
return (
<article>
<h2 className="flex flex-col justify-center items-center text-[1.5em] p-[10px] text-center text-white max-w-[250px] mx-auto bg-gradient-to-b from-[transparent] to-[#00000033] shadow-[0_9px_2px_0_rgba(0,0,0,0.5)] rounded-[5px] my-[2em] perspective-500 ">
실시간 인기명언 <span> Top 100</span>
</h2>
<button className="absolute flex items-center left-[50%] translate-x-[-50%] text-white hover:cursor-pointer z-[20] hover:text-[gold] " onClick={reload}><HiRefresh className='mx-[3px]' />새로고침 </button>
<PopularQuoteList quotes={quotes} />
</article>

)
}
6 changes: 4 additions & 2 deletions src/app/(quotes)/search/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ export default function SearchPage() {
/**
* * 전체, 인물별, 키워드 각 탭(타입) 별로 데이터를 처리하여 상태에 저장하는 함수
* @param items 인물별 && 키워드별 검색 전체 결과가 담긴 배열
* @param totalCounts 인물별 && 키워드별 검색 전체 결과 항목 수를 저장하고 있는 객체
* @param totalCount 인물별 || 키워드별 전체 검색 결과 항목 수를 저장하고 있는 객체
* @param totalCounts 인물별 AND 키워드별 전체 검색 결과 항목 수를 저장하고 있는 객체
* @param totalCount 인물별 OR 키워드별 개별 검색 결과 항목 수를 저장하고 있는 객체
*/
const processByType = useCallback(
(
Expand All @@ -41,11 +41,13 @@ export default function SearchPage() {
totalCount: number,
) => {
switch (type) {
// 전체 검색 결과에 검색된 항목수를 결합
case 'all': {
const mergeItems = { ...items, ...totalCounts }
setItems(mergeItems)
break
}
// 개별 검색 결과에 검색된 항목수를 결합
case 'keyword':
case 'author': {
const mergeItems = { quotes: items, totalCount }
Expand Down
8 changes: 4 additions & 4 deletions src/app/api/quotes/[id]/views/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export async function PATCH(req: NextRequest, res: { params: { id: string } }) {
const db = await openDB();

const selectQuery = `
SELECT COUNT(quote_id) as count
SELECT views
FROM views
WHERE quote_id = $1
`
Expand All @@ -55,10 +55,10 @@ export async function PATCH(req: NextRequest, res: { params: { id: string } }) {
WHERE quote_id = $2
`
const selectResult = await db.query(selectQuery, [id])
const isViews = (selectResult.rows[0].count || 0) > 0
const views = selectResult.rows[0].count
const isViews = (selectResult.rowCount || 0) > 0
const views = isViews ?selectResult.rows[0].views : 0
let viewsTypeToNum = Number(views)


// 게시글을 1번이라도 조회하여 테이블에 등록된 경우
if (isViews) {
Expand Down
26 changes: 26 additions & 0 deletions src/app/api/quotes/populars/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { HTTP_CODE } from "@/app/http-code"
import { openDB } from "@/utils/connect";
import { NextResponse } from "next/server"

export async function GET() {

try {
const db = await openDB();

const selectQuery = `
SELECT *
FROM quotes A
INNER JOIN views B ON A.quote_id = B.quote_id
ORDER BY B.views DESC, A.quote_id ASC
`

const results = await db.query(selectQuery)
const items = results.rows

return NextResponse.json({...HTTP_CODE.OK,quotes:items})

} catch (error) {
console.error('/api/quotes/populars', error)
return NextResponse.json(HTTP_CODE.INTERNAL_SERVER_ERROR)
}
}
8 changes: 4 additions & 4 deletions src/components/UI/common/TtsButton.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { MouseEventHandler } from "react"
import { SlEarphones } from "react-icons/sl"


interface PropsType {
onClickSetText: MouseEventHandler<HTMLButtonElement>
onClickSetText: (quote:string|null) => void
className: string
quote:string | null
}
export default function TtsButton({ onClickSetText, className }: PropsType) {
export default function TtsButton({ onClickSetText, className, quote }: PropsType) {

return (

<button
aria-label="명언 듣기 버튼"
className={className}
onClick={onClickSetText}
onClick={()=>{onClickSetText(quote)}}
>
<SlEarphones className="pr-[2px]" />
</button>
Expand Down
83 changes: 83 additions & 0 deletions src/components/UI/quote/PopularQuoteCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { QuoteType } from "@/app/(quotes)/quotes/populars/page";
import TtsButton from "../common/TtsButton";
import QuoteViews from "./QuoteViews";
import useTTS from "@/custom/useTTS";
import { HiSpeakerphone } from "react-icons/hi";
import QuoteProgress from "./QuoteProgress";
import QuoteDetailMoveButton from "./QuoteDetailMoveButton";
import { viewCounter } from "@/services/data/patch";
import { MouseEvent } from "react";
import toast from "react-hot-toast";
import { useRouter } from "next/navigation";
import gsap from "gsap/all";

interface PropsType {
quoteInfo: QuoteType
}
export default function PopularQuoteCard({ quoteInfo }: PropsType) {

const { isPlaying, progress, readText, setText } = useTTS()

const {push} = useRouter()

const onClickSetText = (quote: string | null) => {
if (!quote) return
setText(quote)
}

// 상세 페이지 이동
const onClickPushAnimation = (e: MouseEvent<HTMLButtonElement>) => {
viewCounter(quoteInfo.quote_id)

const tl = gsap.timeline()
tl.to(e.currentTarget.parentElement, {
scale: 0.8,
duration: 1,
onStart() {
toast('✈ 잠시후, 디테일 명언 카드 페이지로 이동합니다.', {
className: 'font-sans'
})
},
})
tl.to(e.currentTarget.parentElement, {
scale: 0.5,
rotateY: 10,
filter: 'blur(1px)',
borderTopLeftRadius: '5%',
borderBottomLeftRadius: '5%',
boxShadow: '-100px 0 100px 0 tomato'

})
tl.to(e.currentTarget.parentElement, {
x: window.innerWidth,
opacity: 0,
})
tl.to(e.currentTarget.parentElement, {
onComplete() {
push(`/quotes/authors/${quoteInfo.author}/${quoteInfo.quote_id}`)
tl.kill()
},
})
}

return (
<li key={quoteInfo.quote_id}
className={`visible shadow-[inset_0_0_0_3px_white] rounded-[10px] w-[95%] my-[1em] max-w-[500px] bg-transparent px-[15px] py-[35px] mx-auto relative hover:bg-[#d5d5d533]`}>
<blockquote className="text-white mt-[1em] ">
<p>{readText || quoteInfo.quote}</p>
<span className="block font-bold mt-[1em] text-right">
- {quoteInfo.author} -
</span>
</blockquote>
<TtsButton quote={quoteInfo.quote} onClickSetText={onClickSetText} className='absolute right-[3.3em] top-[0.429em] decoration-wavy decoration-[tomato] underline text-[1.1em] hover:shadow-[inset_0_0_0_1px_tomato] p-[4px] py-[5px] text-white ' />
<QuoteDetailMoveButton onClickDetailMove={onClickPushAnimation} />
<QuoteProgress progress={progress} />
<QuoteViews viewCount={quoteInfo.views} />

<p>{isPlaying
? <span className='text-[1.05em] animate-pulse absolute bottom-2 left-2 text-white rounded-[10px] p-[2px] px-[7px] flex items-center'><HiSpeakerphone color='gold' className='mr-[5px]' /> {progress}/100</span>
: <span className='text-[1.05em] animate-none absolute bottom-2 left-2 text-white rounded-[10px] p-[2px] px-[7px]'></span>}</p>

</li>
)
}
20 changes: 20 additions & 0 deletions src/components/UI/quote/PopularQuoteList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'use client'
import { QuoteType } from "@/app/(quotes)/quotes/populars/page"
import ReplaceMessageCard from "../common/ReplaceMessageCard"
import PopularQuoteCard from "./PopularQuoteCard"

interface PropsType {
quotes: QuoteType[]
}
export default function PopularQuoteList({ quotes }: PropsType) {


if (!quotes || quotes?.length < 1) return <ReplaceMessageCard childern='데이터를 조회중입니다..' />
return (
<ul className=" mt-[1em] pt-[2em] flex flex-col flex-wrap w-full perspective-500 transform-style-3d">
{quotes.map(quoteInfo =>
<PopularQuoteCard quoteInfo={quoteInfo} key={quoteInfo.quote_id}/>
)}
</ul>
)
}
2 changes: 1 addition & 1 deletion src/components/UI/quote/QuoteCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ export default function QuoteCard({ item, items, index }: PropsType) {
>

<QuoteProgress progress={progress} />
<TtsButton onClickSetText={onClickSetText} className='absolute right-[3.3em] top-[0.429em] decoration-wavy decoration-[tomato] underline text-[1.1em] hover:shadow-[inset_0_0_0_1px_tomato] p-[4px] py-[5px] text-white ' />
<TtsButton onClickSetText={onClickSetText} className='absolute right-[3.3em] top-[0.429em] decoration-wavy decoration-[tomato] underline text-[1.1em] hover:shadow-[inset_0_0_0_1px_tomato] p-[4px] py-[5px] text-white' quote={null} />
<QuoteDetailMoveButton onClickDetailMove={onClickPushAnimation} />

{pathName.includes('/user-quotes') ? (
Expand Down
3 changes: 0 additions & 3 deletions src/components/UI/quote/QuoteContent.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@



interface PropsType {
[key: string]: string
}
Expand Down
4 changes: 3 additions & 1 deletion src/router.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
HiChartBar,
HiCommandLine,
HiNewspaper,
HiOutlineHandThumbUp,
Expand All @@ -9,12 +10,13 @@ import {
import { HiPhotograph } from 'react-icons/hi'

const navList = [
{ path: '/quotes/populars', label: '실시간 인기명언', icon: HiChartBar },
{ path: '/quotes/topics', label: '주제별 명언', icon: HiNewspaper },
{ path: '/quotes/authors', label: '인물별 명언', icon: HiOutlineUserGroup },
{ path: '/user-quotes', label: '유저 명언', icon: HiOutlineHandThumbUp },
{ path: '/add-wisesaying', label: '명언 쓰기', icon: HiOutlinePencil },
{ path: '/mypage', label: '마이페이지', icon: HiPhotograph },
{ path: '/ai-quote', label: 'AI 명언', icon: HiCommandLine },
{ path: '/ai-quote', label: 'AI 명언 생성', icon: HiCommandLine },
{ path: '/', label: '홈', icon: HiOutlineHomeModern },
]

Expand Down
5 changes: 2 additions & 3 deletions src/services/data/patch.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@

/**
* PATCH | 조회수 계산
* @param quoteId 명언 식별자(id)
*/
export async function viewCounter(quoteId: number) {
export async function viewCounter(quoteId: number | string) {
try {
const response = await fetch('/api/quotes/' + quoteId + '/views',{
const response = await fetch('/api/quotes/' + Number(quoteId) + '/views',{
method:'PATCH'
})
if (!response.ok) throw new Error(response.statusText)
Expand Down
4 changes: 2 additions & 2 deletions src/utils/swr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { getFetcher } from './fetcher'
import toast from 'react-hot-toast'

// reference : https://swr.vercel.app/ko/docs/data-fetching
export const useSwrFetch = (url: string) => {
export const useSwrFetch = (url: string, refreshTimer:number|undefined, isActRefresh:boolean | undefined) => {
const { data, isLoading, error, mutate } = useSWR(url, getFetcher, {
errorRetryCount: 2,
refreshInterval: 60000,
refreshInterval: refreshTimer || 60000,
onError: (error) => {
toast.error('데이터 가져오기 실패:', error.message)
},
Expand Down

0 comments on commit d776dfc

Please sign in to comment.