먼저 한 변의 길이가 n인 달팽이 배열을 만드는 함수를 작성한다.
달팽이 배열 문제는 여러 가지 방법으로 풀 수 있을 것인데, 짧지만 어려운 방법 대신 길지만 직관적인 알고리즘으로 작성했다.
'-1'로 초기화된 배열의 (0, 0)에서 시작하여, for문을 통해 direction(left) 방향으로 계속 이동한다(row와 col을 1씩 더하거나 빼가며). 그러다 다음 이동할 칸이 -1이 아닐 경우(out of range로 인한 undefined이거나 이미 양수로 할당된 칸), direction을 변경한다.
/**
* @param n = 3 일 때,
* 0 1 2
* 7 8 3
* 6 5 4
* 의 달팽이 배열이 나오고,
* @return 값은 [{ row: 0, col: 0 }, { row: 0, col: 1 }, { row: 0, col: 2 }, { row: 1, col: 2 }, ... , { row: 2, col: 2 }]
*/
export function generateSnailPositionArray(n: number): { row: number; col: number }[] {
const snailPositionArray = Array(n ** 2).fill(null);
const snailIndexArray = Array(n ** 2).fill(-1);
let direction: 'left' | 'right' | 'up' | 'bottom' = 'right';
let row = 0;
let col = 0;
for (let count = 0; count < snailIndexArray.length; count++) {
snailPositionArray[count] = { row, col };
snailIndexArray[row * n + col] = count;
switch (direction) {
case 'right':
if (col + 1 < n && snailIndexArray[row * n + col + 1] === -1) {
col += 1;
} else {
direction = 'bottom';
row += 1;
}
break;
case 'bottom':
if (row + 1 < n && snailIndexArray[(row + 1) * n + col] === -1) {
row += 1;
} else {
direction = 'left';
col -= 1;
}
break;
case 'left':
if (col - 1 >= 0 && snailIndexArray[row * n + col - 1] === -1) {
col -= 1;
} else {
direction = 'up';
row -= 1;
}
break;
case 'up':
if (row - 1 >= 0 && snailIndexArray[(row - 1) * n + col] === -1) {
row -= 1;
} else {
direction = 'right';
col += 1;
}
break;
}
}
return snailPositionArray;
}
달팽이 배열에서 필요로 하는 return값의 형태는
arr = [{ row: 0, col: 0 }, { row: 0, col: 1 }, { row: 0, col: 2 }, { row: 1, col: 2 }, ... , { row: 2, col: 2 }]
와 같은 형태이다.
n번째 포스트잇을 arr[n]의 { row, col } 값 만큼 translate 하면 달팽이 모양대로 포스트잇을 나열할 수 있다.
그 후 Card(포스트잇) 컴포넌트를 작성한다.
...
import {
CARD_WIDTH,
FEEDBACK_COLOR_SET,
SNAIL_SIDE_LENGTH,
snailPositionArray,
} from '.../variables';
interface Props {
feedbackList: Feedback[];
}
const FeedbackList = ({ feedbackList }: Props): React.ReactElement => {
return (
<>
{feedbackList.map((feedback, index) => {
const isOutOfRange = index >= SNAIL_SIDE_LENGTH ** 2;
return (
<Item
style={{
zIndex: index === 1 ? Z_INDEX.BASE : 0,
transform: isOutOfRange
? `translate(${-1 * CARD_WIDTH}px, 0)`
: `translate(
${snailPositionArray[index].col * CARD_WIDTH}px,
${snailPositionArray[index].row * CARD_WIDTH}px
)`,
}}
...
>
<div className="card">
<p className="text">{feedback.content}</p>
...
</div>
</Item>
);
})}
</>
);
};
export default React.memo(FeedbackList);
export const Item = styled.div`
position: absolute;
top: 0;
left: 0;
width: ${CARD_WIDTH}px;
height: ${CARD_WIDTH}px;
padding: 12px;
transition: transform 1s ease;
.card {
position: relative;
width: 100%;
height: 100%;
padding: 16px 16px 32px;
...
}
...
`;
핵심 로직은 간단하게 구현할 수 있다.
const isOutOfRange = index >= SNAIL_SIDE_LENGTH ** 2;
return (
<Item
style={{
zIndex: index === 1 ? Z_INDEX.BASE : 0,
transform: isOutOfRange
? `translate(${-1 * CARD_WIDTH}px, 0)`
: `translate(
${snailPositionArray[index].col * CARD_WIDTH}px,
${snailPositionArray[index].row * CARD_WIDTH}px
)`,
}}
...
index번째 포스트잇은 (snailPositionArray[index].col * CARD_WIDTH, snailPositionArray[index].row * CARD_WIDTH) 만큼 translate 해준다.
새로운 피드백이 생겨 배열이 길어지면(> n^2) 달팽이 배열을 벗어나는(isOutOfRange) index가 발생한다. 이 index들은 'translate(${-1 * CARD_WIDTH}px, 0)'와 'overflow: hidden' 을 통해 사용자에게 보이지 않도록 숨겨준다.