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
137 changes: 75 additions & 62 deletions src/app/meeting/photo-capture/_components/CameraView.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useEffect } from 'react'
import React, { useCallback, useEffect, useState, useMemo } from 'react'
import Image from 'next/image'
import Link from 'next/link'
import CameraCaptureButton from '@/assets/CameraCaptureButton.svg'
Expand Down Expand Up @@ -26,6 +26,7 @@ function CameraView({
}: CameraViewProps) {
const { activeTooltip, hideTooltip, showTooltip } = useTooltipStore()
const { currentMission, setCurrentMission } = useMissionStore()
const [videoSize, setVideoSize] = useState({ width: 0, height: 0 })

const adjustVideoSize = useCallback(() => {
if (videoRef.current) {
Expand All @@ -35,33 +36,37 @@ function CameraView({
const containerAspect = container.clientWidth / container.clientHeight
const videoAspect = video.videoWidth / video.videoHeight

let newWidth
let newHeight

if (containerAspect > videoAspect) {
video.style.width = '100%'
video.style.height = 'auto'
newHeight = container.clientHeight
newWidth = newHeight * videoAspect
} else {
video.style.width = 'auto'
video.style.height = '100%'
newWidth = container.clientWidth
newHeight = newWidth / videoAspect
}

setVideoSize({ width: newWidth, height: newHeight })
}
}
}, [])
}, [videoRef])

useEffect(() => {
window.addEventListener('resize', adjustVideoSize)
return () => {
window.removeEventListener('resize', adjustVideoSize)
const video = videoRef.current
if (video) {
const handleLoadedMetadata = () => adjustVideoSize()
video.addEventListener('loadedmetadata', handleLoadedMetadata)
return () =>
video.removeEventListener('loadedmetadata', handleLoadedMetadata)
}
}, [adjustVideoSize])
return undefined
}, [videoRef, adjustVideoSize])

useEffect(() => {
if (videoRef.current) {
videoRef.current.addEventListener('loadedmetadata', adjustVideoSize)
}
return () => {
if (videoRef.current) {
videoRef.current.removeEventListener('loadedmetadata', adjustVideoSize)
}
}
const handleResize = () => adjustVideoSize()
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [adjustVideoSize])

useEffect(() => {
Expand All @@ -70,40 +75,59 @@ function CameraView({
} else {
showTooltip('mission')
}
return undefined
}, [currentMission, hideTooltip, showTooltip])

const missionButton = useMemo(
() => (
<Link href="/create-mission">
<div className="flex justify-between items-center px-3 py-2 rounded-[14px] bg-gray-200 text-body2 text-gray-700">
{currentMission ? (
<>미션 바꾸기</>
) : (
<>
<Image src={PlusGray} alt="Plus Gray" className="w-4 h-4 mr-2" />
미션 추가하기
</>
)}
</div>
{!currentMission && activeTooltip === 'mission' && (
<Tooltip
message="내 사진에 미션을 더 해봐요!"
onClose={hideTooltip}
position="bottom"
arrowClassName="left-12"
className="top-12 left-28"
/>
)}
</Link>
),
[currentMission, activeTooltip, hideTooltip],
)

const missionDisplay = useMemo(
() =>
currentMission ? (
<div className="flex justify-center text-gray-50 bg-point-mint px-3 py-2 rounded-[14px] text-sm">
{currentMission}
<button onClick={() => setCurrentMission(null)} className="ml-2">
X
</button>
</div>
) : (
<div className="flex justify-center text-gray-500 bg-gray-200 px-3 py-2 rounded-[14px] text-sm">
모임의 순간을 담아주세요!
</div>
),
[currentMission, setCurrentMission],
)

return (
<div className="flex flex-col min-h-screen w-full ">
<div className="flex flex-col min-h-screen w-full">
{/* header */}
<div className="p-4">
<div className="flex justify-between items-center h-12 w-full">
<div className="relative">
<Link href="/create-mission">
<div className="flex justify-between items-center px-3 py-2 rounded-[14px] bg-gray-200 text-body2 text-gray-700">
{currentMission ? (
<>미션 바꾸기</>
) : (
<>
<Image
src={PlusGray}
alt="Plus Gray"
className="w-4 h-4 mr-2"
/>
미션 추가하기
</>
)}
</div>
{!currentMission && activeTooltip === 'mission' && (
<Tooltip
message="내 사진에 미션을 더 해봐요!"
onClose={hideTooltip}
position="bottom"
arrowClassName="left-12"
className="top-12 left-28"
/>
)}
</Link>
</div>
<div className="relative">{missionButton}</div>
<div className="flex-1" />
<button
tabIndex={0}
Expand All @@ -126,30 +150,19 @@ function CameraView({
ref={videoRef}
autoPlay
playsInline
className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 min-w-full min-h-full w-auto h-auto max-w-none"
className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 min-w-full min-h-full w-auto h-auto max-w-none object-cover"
style={{
transform: `${isRearCamera ? '' : 'scaleX(-1)'} translate(-50%, -50%)`,
width: `${videoSize.width}px`,
height: `${videoSize.height}px`,
transform: `translate(-50%, -50%) ${isRearCamera ? '' : 'scaleX(-1)'}`,
}}
/>
</div>
</div>

{/* footer */}
<div className="flex flex-col mt-auto mb-9">
<div className="flex justify-center">
{currentMission ? (
<div className="flex justify-center text-gray-50 bg-point-mint px-3 py-2 rounded-[14px] text-sm">
{currentMission}
<button onClick={() => setCurrentMission(null)} className="ml-2">
X
</button>
</div>
) : (
<div className="flex justify-center text-gray-500 bg-gray-200 px-3 py-2 rounded-[14px] text-sm">
모임의 순간을 담아주세요!
</div>
)}
</div>
<div className="flex justify-center">{missionDisplay}</div>

<div className="flex items-center justify-center w-full mt-5">
<button
Expand Down
100 changes: 65 additions & 35 deletions src/hooks/useCamera.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ function useCamera(setPhoto: (photo: string | null) => void) {
const [isRearCamera, setIsRearCamera] = useState(true)
const videoRef = useRef<HTMLVideoElement>(null)
const canvasRef = useRef<HTMLCanvasElement>(null)
const streamRef = useRef<MediaStream | null>(null)
const isMountedRef = useRef(true)

const openCamera = useCallback(async () => {
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
Expand All @@ -27,28 +29,54 @@ function useCamera(setPhoto: (photo: string | null) => void) {

const stream = await navigator.mediaDevices.getUserMedia(constraints)

if (videoRef.current) {
if (videoRef.current.srcObject) {
const existingStream = videoRef.current.srcObject as MediaStream
existingStream.getTracks().forEach((track) => track.stop())
videoRef.current.srcObject = null
}
if (!isMountedRef.current) {
stream.getTracks().forEach((track) => track.stop())
return
}

videoRef.current.srcObject = stream
streamRef.current = stream

videoRef.current.onloadedmetadata = () => {
if (videoRef.current) {
videoRef.current.play().catch((err) => {
console.error('비디오를 재생할 수 없습니다:', err)
})
if (videoRef.current && isMountedRef.current) {
videoRef.current.srcObject = stream
try {
await videoRef.current.play()
} catch (playError) {
if (playError instanceof Error && playError.name !== 'AbortError') {
console.error('비디오 재생 중 오류 발생:', playError)
}
}
}
} catch (err) {
console.error('카메라를 열 수 없습니다:', err)
if (
isMountedRef.current &&
err instanceof Error &&
err.name !== 'AbortError'
) {
console.error('카메라를 열 수 없습니다:', err)
}
}
}, [isRearCamera])

const drawImageOnCanvas = useCallback(
(
context: CanvasRenderingContext2D,
video: HTMLVideoElement,
size: number,
sx: number,
sy: number,
sSize: number,
) => {
if (!isRearCamera) {
context.scale(-1, 1)
context.drawImage(video, sx, sy, sSize, sSize, -size, 0, size, size)
context.scale(-1, 1)
} else {
context.drawImage(video, sx, sy, sSize, sSize, 0, 0, size, size)
}
},
[isRearCamera],
)

const takePicture = useCallback(() => {
const canvas = canvasRef.current
const video = videoRef.current
Expand All @@ -65,41 +93,43 @@ function useCamera(setPhoto: (photo: string | null) => void) {
const context = canvas.getContext('2d')
if (context) {
context.scale(dpr, dpr)
}

const scaleX = video.videoWidth / videoRect.width
const scaleY = video.videoHeight / videoRect.height
const scaleX = video.videoWidth / videoRect.width
const scaleY = video.videoHeight / videoRect.height
const centerX = videoRect.width / 2
const centerY = videoRect.height / 2
const sx = (centerX - size / 2) * scaleX
const sy = (centerY - size / 2) * scaleY
const sSize = size * scaleX

const centerX = videoRect.width / 2
const centerY = videoRect.height / 2
drawImageOnCanvas(context, video, size, sx, sy, sSize)

const sx = (centerX - size / 2) * scaleX
const sy = (centerY - size / 2) * scaleY
const sSize = size * scaleX
const dataUrl = canvas.toDataURL('image/jpeg', 0.95)
setPhoto(dataUrl)

if (!isRearCamera) {
context?.scale(-1, 1)
context?.drawImage(video, sx, sy, sSize, sSize, -size, 0, size, size)
context?.scale(-1, 1)
} else {
context?.drawImage(video, sx, sy, sSize, sSize, 0, 0, size, size)
const stream = video.srcObject as MediaStream
stream.getTracks().forEach((track) => track.stop())
setIsCameraOpen(false)
}

const dataUrl = canvas.toDataURL('image/jpeg', 0.95)
setPhoto(dataUrl)

const stream = video.srcObject as MediaStream
stream.getTracks().forEach((track) => track.stop())
setIsCameraOpen(false)
}
}, [isRearCamera])

const toggleCamera = useCallback(() => {
setIsRearCamera((prevState) => !prevState)
if (streamRef.current) {
streamRef.current.getTracks().forEach((track) => track.stop())
}
setIsRearCamera((prev) => !prev)
}, [])

useEffect(() => {
isMountedRef.current = true
openCamera()
return () => {
isMountedRef.current = false
if (streamRef.current) {
streamRef.current.getTracks().forEach((track) => track.stop())
}
}
}, [isRearCamera, openCamera])

return {
Expand Down