Skip to content

Commit 475ab09

Browse files
authored
Merge pull request #118 from dnd-side-project/fix/camera-facing-user-mode-bug
fix: 전면 카메라(셀카모드) 버그 수정
2 parents 5d03b5a + a2cbf30 commit 475ab09

File tree

2 files changed

+140
-97
lines changed

2 files changed

+140
-97
lines changed

src/app/meeting/photo-capture/_components/CameraView.tsx

Lines changed: 75 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback, useEffect } from 'react'
1+
import React, { useCallback, useEffect, useState, useMemo } from 'react'
22
import Image from 'next/image'
33
import Link from 'next/link'
44
import CameraCaptureButton from '@/assets/CameraCaptureButton.svg'
@@ -26,6 +26,7 @@ function CameraView({
2626
}: CameraViewProps) {
2727
const { activeTooltip, hideTooltip, showTooltip } = useTooltipStore()
2828
const { currentMission, setCurrentMission } = useMissionStore()
29+
const [videoSize, setVideoSize] = useState({ width: 0, height: 0 })
2930

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

39+
let newWidth
40+
let newHeight
41+
3842
if (containerAspect > videoAspect) {
39-
video.style.width = '100%'
40-
video.style.height = 'auto'
43+
newHeight = container.clientHeight
44+
newWidth = newHeight * videoAspect
4145
} else {
42-
video.style.width = 'auto'
43-
video.style.height = '100%'
46+
newWidth = container.clientWidth
47+
newHeight = newWidth / videoAspect
4448
}
49+
50+
setVideoSize({ width: newWidth, height: newHeight })
4551
}
4652
}
47-
}, [])
53+
}, [videoRef])
4854

4955
useEffect(() => {
50-
window.addEventListener('resize', adjustVideoSize)
51-
return () => {
52-
window.removeEventListener('resize', adjustVideoSize)
56+
const video = videoRef.current
57+
if (video) {
58+
const handleLoadedMetadata = () => adjustVideoSize()
59+
video.addEventListener('loadedmetadata', handleLoadedMetadata)
60+
return () =>
61+
video.removeEventListener('loadedmetadata', handleLoadedMetadata)
5362
}
54-
}, [adjustVideoSize])
63+
return undefined
64+
}, [videoRef, adjustVideoSize])
5565

5666
useEffect(() => {
57-
if (videoRef.current) {
58-
videoRef.current.addEventListener('loadedmetadata', adjustVideoSize)
59-
}
60-
return () => {
61-
if (videoRef.current) {
62-
videoRef.current.removeEventListener('loadedmetadata', adjustVideoSize)
63-
}
64-
}
67+
const handleResize = () => adjustVideoSize()
68+
window.addEventListener('resize', handleResize)
69+
return () => window.removeEventListener('resize', handleResize)
6570
}, [adjustVideoSize])
6671

6772
useEffect(() => {
@@ -70,40 +75,59 @@ function CameraView({
7075
} else {
7176
showTooltip('mission')
7277
}
78+
return undefined
7379
}, [currentMission, hideTooltip, showTooltip])
7480

81+
const missionButton = useMemo(
82+
() => (
83+
<Link href="/create-mission">
84+
<div className="flex justify-between items-center px-3 py-2 rounded-[14px] bg-gray-200 text-body2 text-gray-700">
85+
{currentMission ? (
86+
<>미션 바꾸기</>
87+
) : (
88+
<>
89+
<Image src={PlusGray} alt="Plus Gray" className="w-4 h-4 mr-2" />
90+
미션 추가하기
91+
</>
92+
)}
93+
</div>
94+
{!currentMission && activeTooltip === 'mission' && (
95+
<Tooltip
96+
message="내 사진에 미션을 더 해봐요!"
97+
onClose={hideTooltip}
98+
position="bottom"
99+
arrowClassName="left-12"
100+
className="top-12 left-28"
101+
/>
102+
)}
103+
</Link>
104+
),
105+
[currentMission, activeTooltip, hideTooltip],
106+
)
107+
108+
const missionDisplay = useMemo(
109+
() =>
110+
currentMission ? (
111+
<div className="flex justify-center text-gray-50 bg-point-mint px-3 py-2 rounded-[14px] text-sm">
112+
{currentMission}
113+
<button onClick={() => setCurrentMission(null)} className="ml-2">
114+
X
115+
</button>
116+
</div>
117+
) : (
118+
<div className="flex justify-center text-gray-500 bg-gray-200 px-3 py-2 rounded-[14px] text-sm">
119+
모임의 순간을 담아주세요!
120+
</div>
121+
),
122+
[currentMission, setCurrentMission],
123+
)
124+
75125
return (
76-
<div className="flex flex-col min-h-screen w-full ">
126+
<div className="flex flex-col min-h-screen w-full">
77127
{/* header */}
78128
<div className="p-4">
79129
<div className="flex justify-between items-center h-12 w-full">
80-
<div className="relative">
81-
<Link href="/create-mission">
82-
<div className="flex justify-between items-center px-3 py-2 rounded-[14px] bg-gray-200 text-body2 text-gray-700">
83-
{currentMission ? (
84-
<>미션 바꾸기</>
85-
) : (
86-
<>
87-
<Image
88-
src={PlusGray}
89-
alt="Plus Gray"
90-
className="w-4 h-4 mr-2"
91-
/>
92-
미션 추가하기
93-
</>
94-
)}
95-
</div>
96-
{!currentMission && activeTooltip === 'mission' && (
97-
<Tooltip
98-
message="내 사진에 미션을 더 해봐요!"
99-
onClose={hideTooltip}
100-
position="bottom"
101-
arrowClassName="left-12"
102-
className="top-12 left-28"
103-
/>
104-
)}
105-
</Link>
106-
</div>
130+
<div className="relative">{missionButton}</div>
107131
<div className="flex-1" />
108132
<button
109133
tabIndex={0}
@@ -126,30 +150,19 @@ function CameraView({
126150
ref={videoRef}
127151
autoPlay
128152
playsInline
129-
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"
153+
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"
130154
style={{
131-
transform: `${isRearCamera ? '' : 'scaleX(-1)'} translate(-50%, -50%)`,
155+
width: `${videoSize.width}px`,
156+
height: `${videoSize.height}px`,
157+
transform: `translate(-50%, -50%) ${isRearCamera ? '' : 'scaleX(-1)'}`,
132158
}}
133159
/>
134160
</div>
135161
</div>
136162

137163
{/* footer */}
138164
<div className="flex flex-col mt-auto mb-9">
139-
<div className="flex justify-center">
140-
{currentMission ? (
141-
<div className="flex justify-center text-gray-50 bg-point-mint px-3 py-2 rounded-[14px] text-sm">
142-
{currentMission}
143-
<button onClick={() => setCurrentMission(null)} className="ml-2">
144-
X
145-
</button>
146-
</div>
147-
) : (
148-
<div className="flex justify-center text-gray-500 bg-gray-200 px-3 py-2 rounded-[14px] text-sm">
149-
모임의 순간을 담아주세요!
150-
</div>
151-
)}
152-
</div>
165+
<div className="flex justify-center">{missionDisplay}</div>
153166

154167
<div className="flex items-center justify-center w-full mt-5">
155168
<button

src/hooks/useCamera.ts

Lines changed: 65 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ function useCamera(setPhoto: (photo: string | null) => void) {
55
const [isRearCamera, setIsRearCamera] = useState(true)
66
const videoRef = useRef<HTMLVideoElement>(null)
77
const canvasRef = useRef<HTMLCanvasElement>(null)
8+
const streamRef = useRef<MediaStream | null>(null)
9+
const isMountedRef = useRef(true)
810

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

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

30-
if (videoRef.current) {
31-
if (videoRef.current.srcObject) {
32-
const existingStream = videoRef.current.srcObject as MediaStream
33-
existingStream.getTracks().forEach((track) => track.stop())
34-
videoRef.current.srcObject = null
35-
}
32+
if (!isMountedRef.current) {
33+
stream.getTracks().forEach((track) => track.stop())
34+
return
35+
}
3636

37-
videoRef.current.srcObject = stream
37+
streamRef.current = stream
3838

39-
videoRef.current.onloadedmetadata = () => {
40-
if (videoRef.current) {
41-
videoRef.current.play().catch((err) => {
42-
console.error('비디오를 재생할 수 없습니다:', err)
43-
})
39+
if (videoRef.current && isMountedRef.current) {
40+
videoRef.current.srcObject = stream
41+
try {
42+
await videoRef.current.play()
43+
} catch (playError) {
44+
if (playError instanceof Error && playError.name !== 'AbortError') {
45+
console.error('비디오 재생 중 오류 발생:', playError)
4446
}
4547
}
4648
}
4749
} catch (err) {
48-
console.error('카메라를 열 수 없습니다:', err)
50+
if (
51+
isMountedRef.current &&
52+
err instanceof Error &&
53+
err.name !== 'AbortError'
54+
) {
55+
console.error('카메라를 열 수 없습니다:', err)
56+
}
4957
}
5058
}, [isRearCamera])
5159

60+
const drawImageOnCanvas = useCallback(
61+
(
62+
context: CanvasRenderingContext2D,
63+
video: HTMLVideoElement,
64+
size: number,
65+
sx: number,
66+
sy: number,
67+
sSize: number,
68+
) => {
69+
if (!isRearCamera) {
70+
context.scale(-1, 1)
71+
context.drawImage(video, sx, sy, sSize, sSize, -size, 0, size, size)
72+
context.scale(-1, 1)
73+
} else {
74+
context.drawImage(video, sx, sy, sSize, sSize, 0, 0, size, size)
75+
}
76+
},
77+
[isRearCamera],
78+
)
79+
5280
const takePicture = useCallback(() => {
5381
const canvas = canvasRef.current
5482
const video = videoRef.current
@@ -65,41 +93,43 @@ function useCamera(setPhoto: (photo: string | null) => void) {
6593
const context = canvas.getContext('2d')
6694
if (context) {
6795
context.scale(dpr, dpr)
68-
}
6996

70-
const scaleX = video.videoWidth / videoRect.width
71-
const scaleY = video.videoHeight / videoRect.height
97+
const scaleX = video.videoWidth / videoRect.width
98+
const scaleY = video.videoHeight / videoRect.height
99+
const centerX = videoRect.width / 2
100+
const centerY = videoRect.height / 2
101+
const sx = (centerX - size / 2) * scaleX
102+
const sy = (centerY - size / 2) * scaleY
103+
const sSize = size * scaleX
72104

73-
const centerX = videoRect.width / 2
74-
const centerY = videoRect.height / 2
105+
drawImageOnCanvas(context, video, size, sx, sy, sSize)
75106

76-
const sx = (centerX - size / 2) * scaleX
77-
const sy = (centerY - size / 2) * scaleY
78-
const sSize = size * scaleX
107+
const dataUrl = canvas.toDataURL('image/jpeg', 0.95)
108+
setPhoto(dataUrl)
79109

80-
if (!isRearCamera) {
81-
context?.scale(-1, 1)
82-
context?.drawImage(video, sx, sy, sSize, sSize, -size, 0, size, size)
83-
context?.scale(-1, 1)
84-
} else {
85-
context?.drawImage(video, sx, sy, sSize, sSize, 0, 0, size, size)
110+
const stream = video.srcObject as MediaStream
111+
stream.getTracks().forEach((track) => track.stop())
112+
setIsCameraOpen(false)
86113
}
87-
88-
const dataUrl = canvas.toDataURL('image/jpeg', 0.95)
89-
setPhoto(dataUrl)
90-
91-
const stream = video.srcObject as MediaStream
92-
stream.getTracks().forEach((track) => track.stop())
93-
setIsCameraOpen(false)
94114
}
95115
}, [isRearCamera])
96116

97117
const toggleCamera = useCallback(() => {
98-
setIsRearCamera((prevState) => !prevState)
118+
if (streamRef.current) {
119+
streamRef.current.getTracks().forEach((track) => track.stop())
120+
}
121+
setIsRearCamera((prev) => !prev)
99122
}, [])
100123

101124
useEffect(() => {
125+
isMountedRef.current = true
102126
openCamera()
127+
return () => {
128+
isMountedRef.current = false
129+
if (streamRef.current) {
130+
streamRef.current.getTracks().forEach((track) => track.stop())
131+
}
132+
}
103133
}, [isRearCamera, openCamera])
104134

105135
return {

0 commit comments

Comments
 (0)