메인 페이지의 핵심 기능인 방 프리뷰는 사용자의 방을 중심으로 하우스메이트(팔로잉)의 방을 시각적으로 보여주는 기능이다. 3D 모델링된 방을 벌집 구조로 배치하여 한눈에 확인할 수 있도록 구현해야 한다.
📌ㅤ제약 및 구현 조건
- 방들이 겹치거나 중간에 빈 공간이 생기지 않아야 한다.
- 규칙성 있게 확장되어야 하며, 나의 방은 항상 중앙에 위치해야 한다.
- 헥사곤 좌표계를 이용하여 배치한다.
getFollowing() 호출getRoomById(user.id) 호출rooms 상태로 저장useEffect(() => {
const fetchData = async () => {
try {
const followingUsers = await getFollowing();
const roomData = await Promise.all(
followingUsers.map((user) => getRoomById(user.id))
);
setRooms(roomData);
} catch (error) {
console.error('방 정보 로딩 실패', error);
}
};
fetchData();
}, []);
(q,r,s) 좌표를 2D (x,y,z) 좌표로 변환const getHexPosition = (index: number) => {
const row = Math.floor(index / 3);
const col = index % 3;
const x = col * 1.5; // 간격 조정
const z = row * 1.3;
return [x, 0, z];
};
getHexPosition(index) 함수를 활용해 위치 계산Room 컴포넌트를 활용해 방을 배치<Canvas>
{rooms.map((room, index) => (
<Room
key={room.id}
modelPath={room.modelPath}
userId={room.userId}
position={getHexPosition(index)}
scale={0.5} // 메인에서는 작게
onClick={() => navigate(`/room/${room.userId}`)}
/>
))}
</Canvas>
📍 역할
- 현재 로그인한 사용자의 방 정보와 팔로잉한 사용자의 방 정보를 불러와
rooms상태에 저장- 방 테마 정보를 매핑하여
modelPath추가
export default function useRooms(limit = 30, myUserId: number) {
const [rooms, setRooms] = useState<Room[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const fetchData = async () => {
try {
const houseMateUsers = await housemateAPI.getFollowing(0, limit);
const roomData = await Promise.all(
houseMateUsers?.housemates?.map((mate: Housemate) =>
roomAPI.getRoomById(mate.userId)
)
);
const modelRooms = roomData.map((room) => {
const themeKey = mapThemeKeyToFullThemeKey(room.theme);
return { ...room, modelPath: FullThemeData[themeKey].modelPath };
});
setRooms(modelRooms);
} catch (error) {
...에러처리 코드
};
fetchData();
}, [limit, myUserId]);
return { rooms, loading, error };
}
(1) 로그인한 사용자의 방 정보 가져오기
roomAPI.getRoomById(myUserId) 호출mapThemeKeyToFullThemeKey 를 이용해 테마 키를 변환 후, modelPath 추가(2) 팔로잉한 유저들의 방 정보 가져오기
housemateAPI.getFollowing(0, limit) 호출(3) 데이터 저장
rooms 상태로 업데이트📍 역할
rooms배열을 헥사곤 구조로 배치- 중앙을 기준으로 점진적으로 확장하며 배치
const directions: Position[] = [
[-1, 0, 1], [1, 0, -1], [0, -1, 1],
[1, -1, 0], [-1, 1, 0], [0, 1, -1],
];
const expansions: Expansion[] = [
{ directionIndex: 2, dir: [-1, 0, 1], side: 'left' },
{ directionIndex: 0, dir: [-1, 0, 1], side: 'left' },
{ directionIndex: 4, dir: [-1, 0, 1], side: 'left' },
{ directionIndex: 3, dir: [1, 0, -1], side: 'right' },
{ directionIndex: 1, dir: [1, 0, -1], side: 'right' },
{ directionIndex: 5, dir: [1, 0, -1], side: 'right' },
];
헥사곤 그리드의 새로운 링을 확장할 때 사용하며, 기존 링을 기준으로 새로운 헥사곤 방을 배치하는 역할을 한다. 원래는
useHexagonGrid에서 코드 분리가 안되어 있었지만, 로직이 복잡해지는 관계로 따로 분리하여 사용하고 있다.
function expandRing(
expansion: Expansion, // 확장 규칙
previousRing: number[], // 이전 링에 속한 방의 인덱스 배열
result: RoomPosition[], // 전체 배치된 방들의 목록
visited: Set<string>, // 방문한 방 좌표를 기록한 Set
rooms: Room[], // 배치할 전체 방 목록
roomIndex: number, // 현재 배치 중인 방의 인덱스
placedInRing: number, // 현재 링에 배치된 방 개수
positionsInRing: number // 현재 링에 배치할 수 있는 최대 방 개수
)
// 헥사곤 그리드 확장 함수
function expandRing(...): { roomIndex: number; placedInRing: number } {
const { directionIndex, dir } = expansion;
if (placedInRing >= positionsInRing || roomIndex >= rooms.length)
return { roomIndex, placedInRing };
for (let i = 0; i < previousRing.length; i++) {
const baseIndex = previousRing[(directionIndex + i) % previousRing.length];
const basePos = result[baseIndex].position;
const [dx, dy, dz] = dir;
const newPos: Position = [basePos[0] + dx, basePos[1] + dy, basePos[2] + dz];
const posKey = `${newPos[0]},${newPos[1]},${newPos[2]}`;
if (!visited.has(posKey) && newPos[0] + newPos[1] + newPos[2] === 0) {
visited.add(posKey);
result.push({ position: newPos, room: rooms[roomIndex] });
return { roomIndex: roomIndex + 1, placedInRing: placedInRing + 1 };
}
}
return { roomIndex, placedInRing };
}
(1) 배치 종료 조건 체크
(2) 이전 링을 기준으로 새로운 방 배치
(3) 새로운 위치 계산
newPosposKey 문자열로 변환해 중복 체크에 사용(4) 중복 체크 및 방 배치
x + y + z = 0 을 만족하는 경우만 추가roomIndex 와 placedInRing 값을 증가const result: RoomPosition[] = [];
const visited = new Set<string>();
const ringRooms: number[][] = [[]];
result : 배치된 방들의 목록visited : 방문한 좌표를 저장하여 중복 방지ringRooms : 링별로 방 인덱스를 저장하는 배열// 중앙에 첫 번째 방 배치
result.push({ position: [0, 0, 0], room: rooms[0] });
visited.add('0,0,0');
ringRooms[0] = [0];
let roomIndex = 1;
// 첫 번째 링 배치 (최대 6개)
if (roomIndex < rooms.length) {
const firstRingIndices: number[] = [];
for (let dir = 0; dir < 6 && roomIndex < rooms.length; dir++) {
const [dx, dy, dz] = directions[dir];
const posKey = `${dx},${dy},${dz}`;
if (!visited.has(posKey)) {
visited.add(posKey);
result.push({ position: [dx, dy, dz], room: rooms[roomIndex] });
firstRingIndices.push(roomIndex);
roomIndex++;
}
}
ringRooms.push(firstRingIndices);
}
(1) 초기 방 배치
(0,0,0) 에 배치// 이후 링 확장
let ring = 1;
while (roomIndex < rooms.length) {
const positionsInRing = Math.min(rooms.length - roomIndex, 6);
let placedInRing = 0;
const previousRing = ringRooms[ring];
if (!previousRing) break;
for (const expansion of expansions) {
const updated = expandRing(
...매개변수
);
roomIndex = updated.roomIndex;
placedInRing = updated.placedInRing;
}
if (placedInRing > 0) {
ringRooms.push(result.slice(roomIndex - placedInRing, roomIndex).map((_, i) => roomIndex - placedInRing + i));
ring++;
}
...
(2) 추가 방 배치
expandRing() 을 사용하여 각 방향에 배치 가능 여부 확인 후 추가ringRooms 에 추가// 2D 좌표 변환 (q, r -> x, y 변환)
return result.map(({ position: [q, r], room }) => {
const x = centerX + width * (q + r / 2);
const y = centerY - height * (2.98 / 4.2) * r;
return { room, position: [x, y, r * 0.7] };
});
}, [rooms, centerX, centerY]);
(3) 위치 변환
q, r, s 좌표계를 화면 x, y 좌표로 변환ㅤ➡️ㅤ헥사곤 좌표계를 2D 좌표로 변환📍 역할
useRooms에서 가져온 방 데이터를useHexagonGrid로 배치 후, Three.js로 렌더링Canvas를 활용하여 방 모델을 배치하고 마우스 이벤트를 처리
export default function HiveRooms({ myUserId }) {
const { rooms } = useRooms(30, myUserId);
const positionedRooms = useHexagonGrid(rooms, 0, 0);
const [hoveredRoom, setHoveredRoom] = useState<number | null>(null);
return (
<div className='w-full h-screen relative'>
<Canvas camera={{ position: [0, 4, 10], fov: 25 }} shadows>
<RoomLighting />
{positionedRooms.map(({ room, position }, index) => (
<group
key={index}
position={position}
onPointerOver={() => setHoveredRoom(index)}
onPointerOut={() => setHoveredRoom(null)}>
<HiveRoomModel room={room} />
</group>
))}
</Canvas>
{hoveredRoom !== null && (
<div className='absolute bottom-22 left-1/2 transform -translate-x-1/2'>
✊🏻 똑똑! {rooms[hoveredRoom]?.nickname}의 방에 들어가실래요?
</div>
)}
</div>
);
}
(1) 방 데이터 가져오기
(2) 헥사곤 그리드 위치 계산
(3) Three.js로 방 모델 렌더링
Canvas 내부에서 HiveRoomModel 을 반복 렌더링onPointerOver, onPointerOut을 통해 호버 효과 추가(4) 마우스 이벤트 처리
mousedown, mousemove, mouseup 이벤트를 활용해 드래그/클릭 감지
👉 UX 개선 포인트
✅ㅤ프리뷰 기능의 직관적인 사용을 돕기 위해 가이드 애니메이션 추가
- 마우스 왼쪽 클릭으로 드래그 가능
- 휠 스크롤을 사용해 줌 인/아웃 조작 가능