https://velog.io/@kimbyungchan/canvas-animation
https://velog.io/@kimbyungchan/canvas-mouse-interaction
https://velog.io/@kimbyungchan/canvas-fireworks
์ด์ ๊ธ์ ์ฐธ๊ณ ํ์๋ฉด ๊ธฐ๋ณธ์ ์ธ ๊ตฌ์กฐ๋ฅผ ์ดํดํ๋๋ฐ ๋์์ด๋ฉ๋๋ค.
์คํํฌ๋ํํธ์์ ์ ๋์ ์ ํํ๋ ๋ฐฉ๋ฒ์ค ํ๋์ธ ๋๋๊ทธ๋ฅผ ๋จผ์ ๊ตฌํํด๋ด ์๋ค.
// App.ts
import Time from './Time';
import EntityManager from './EntityManager';
import Vector from './Vector';
export default class App {
static instance: App;
ref: HTMLElement;
canvas: HTMLCanvasElement;
context: CanvasRenderingContext2D;
handleRequestFrame: number | null = null;
entityManager: EntityManager;
isPressed: boolean = false;
mouseDownPosition: Vector = new Vector(0, 0);
mousePosition: Vector = new Vector(0, 0);
mouseUpPosition: Vector = new Vector(0, 0);
constructor(ref: HTMLElement) {
App.instance = this;
this.ref = ref;
this.canvas = document.createElement('canvas');
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
this.context = this.canvas.getContext('2d')!;
this.entityManager = new EntityManager();
this.ref.appendChild(this.canvas);
}
onMouseDown = (e: MouseEvent) => {
this.isPressed = true;
this.mouseDownPosition = new Vector(e.clientX, e.clientY);
}
onMouseMove = (e: MouseEvent) => {
this.mousePosition = new Vector(e.clientX,e.clientY);
}
onMouseUp = (e: MouseEvent) => {
this.isPressed = false;
this.mouseUpPosition = new Vector(e.clientX, e.clientY);
}
play = () => {
Time.start();
window.addEventListener('mousedown', this.onMouseDown);
window.addEventListener('mousemove', this.onMouseMove);
window.addEventListener('mouseup', this.onMouseUp);
this.handleRequestFrame = window.requestAnimationFrame(this.onEnterFrame);
}
pause = () => {
if (this.handleRequestFrame === null) {
return;
}
window.cancelAnimationFrame(this.handleRequestFrame);
}
onEnterFrame = () => {
Time.update();
this.entityManager.update();
this.entityManager.render(this.context);
this.handleRequestFrame = window.requestAnimationFrame(this.onEnterFrame);
}
}
๋ง์ฐ์ค ์์, ํ์ฌ, ๋ ์ขํ๋ฅผ ์ ์ฅํ ๋ณ์๋ฅผ ๋ง๋ค๊ณ
App ํด๋์ค์ ๊ฐํธํ๊ฒ ์ ๊ทผํ ์ ์๋๋ก ์ฑ๊ธํค ํจํด์ผ๋ก ๋ง๋ค์์ต๋๋ค.
// DragArea.ts
import Entity from './Entity';
import Vector from './Vector';
import App from './App';
export default class DragArea extends Entity {
constructor(position: Vector) {
super(position);
}
update() {
this.position = App.instance.mouseDownPosition;
}
render(context: CanvasRenderingContext2D) {
if (!App.instance.isPressed) {
return;
}
context.beginPath();
context.strokeStyle = '#00ff00';
context.lineWidth = 2;
context.rect(this.position.x, this.position.y, App.instance.mousePosition.x - this.position.x, App.instance.mousePosition.y - this.position.y);
context.stroke();
}
}
App์ ๋ง์ฐ์ค ์ขํ๋ฅผ ๊ฐ์ ธ์ ์ฌ๊ฐํ์ ๊ทธ๋ ธ์ต๋๋ค. ๊ทธ๋ฆฌ๊ณ ํด๋น ์ํฐํฐ๋ฅผ ์ถ๊ฐํด์ฃผ์ด์ผํฉ๋๋ค.
// index.ts
import App from './App';
import DragArea from './DragArea';
import Vector from './Vector';
window.addEventListener('load', () => {
const app = new App(document.body);
const dragArea = new DragArea(new Vector(0, 0))
app.entityManager.addEntity(dragArea);
app.play();
});
์ด์ ๋๋๊ทธ๋ฅผ ํด๋ณด์๋ฉด
์ฑ๊ณต์ ์ผ๋ก ๋๋๊ทธ์์ญ์ด ๋ณด์ด๋๊ฑธ ํ์ธํ์ค์์์ต๋๋ค.
// Unit.ts
import Entity from "./Entity";
import Vector from "./Vector";
function drawEllipse(
context: CanvasRenderingContext2D,
x: number,
y: number,
w: number,
h: number
) {
const kappa = 0.5522848,
ox = (w / 2) * kappa,
oy = (h / 2) * kappa,
xe = x + w,
ye = y + h,
xm = x + w / 2,
ym = y + h / 2;
context.beginPath();
context.moveTo(x, ym);
context.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
context.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
context.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
context.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
context.closePath();
context.stroke();
}
export default class Unit extends Entity {
radius: number;
speed: number;
isSelected: boolean = true;
constructor(position: Vector) {
super(position);
this.radius = 15;
this.speed = 100;
}
update() {}
render(context: CanvasRenderingContext2D) {
if (this.isSelected) {
context.beginPath();
context.lineWidth = 2;
context.strokeStyle = "#0f0";
drawEllipse(
context,
this.position.x - this.radius,
this.position.y + this.radius * 0.75,
this.radius * 2,
this.radius * 0.75
);
}
context.beginPath();
context.fillStyle = "#000";
context.arc(this.position.x, this.position.y, this.radius, 0, Math.PI * 2);
context.fill();
}
}
๊ธฐ๋ณธ์ ์ธ ์คํฏ speed, radius ๋ฅผ ์ถ๊ฐํ๊ณ
์ ํ๋์๋์ง ํ์ธํ isSelected๋ ์ถ๊ฐํด์ฃผ์์ต๋๋ค.
๋์ค์ ๋ค๋ฅธ ์ ๋์ ๋ง๋ค๋ ํด๋นํด๋์ค๋ฅผ ์์๋ฐ์์ ๋ง๋ค๋ฉด ๋๊ฒ ์ฃ ?
// index.ts
import App from './App';
import DragArea from './DragArea';
import Vector from './Vector';
import Unit from './Unit';
window.addEventListener('load', () => {
const app = new App(document.body);
const dragArea = new DragArea(new Vector(0, 0))
const unit = new Unit(new Vector(app.canvas.width * 0.5, app.canvas.height * 0.5));
app.entityManager.addEntity(dragArea);
app.entityManager.addEntity(unit);
app.play();
});
๋์ถฉ ์๋ฌด๊ณณ์์๋ ์ ๋์ ์์ฑํด์ ์ถ๊ฐํด์ค๋๋ค.
// App.ts
import Time from "./Time";
import EntityManager from "./EntityManager";
import Vector from "./Vector";
import Unit from "src/Unit";
export default class App {
static instance: App;
ref: HTMLElement;
canvas: HTMLCanvasElement;
context: CanvasRenderingContext2D;
handleRequestFrame: number | null = null;
entityManager: EntityManager;
isPressed: boolean = false;
mouseDownPosition: Vector = new Vector(0, 0);
mousePosition: Vector = new Vector(0, 0);
mouseUpPosition: Vector = new Vector(0, 0);
selectedUnits: Array<Unit> = [];
constructor(ref: HTMLElement) {
App.instance = this;
this.ref = ref;
this.canvas = document.createElement("canvas");
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
this.context = this.canvas.getContext("2d")!;
this.entityManager = new EntityManager();
this.ref.appendChild(this.canvas);
}
onMouseDown = (e: MouseEvent) => {
this.isPressed = true;
this.mouseDownPosition = new Vector(e.clientX, e.clientY);
};
onMouseMove = (e: MouseEvent) => {
this.mousePosition = new Vector(e.clientX, e.clientY);
};
onMouseUp = (e: MouseEvent) => {
this.isPressed = false;
this.mouseUpPosition = new Vector(e.clientX, e.clientY);
const startX = Math.min(this.mouseDownPosition.x, this.mouseUpPosition.x);
const endX = Math.max(this.mouseDownPosition.x, this.mouseUpPosition.x);
const startY = Math.min(this.mouseDownPosition.y, this.mouseUpPosition.y);
const endY = Math.max(this.mouseDownPosition.y, this.mouseUpPosition.y);
const units = this.entityManager.entities.filter(
(entity) => entity instanceof Unit
) as Unit[];
this.selectedUnits = units.filter((entity: Unit) => {
return (
entity.position.x >= startX &&
entity.position.x <= endX &&
entity.position.y >= startY &&
entity.position.y <= endY
);
});
for (let i = 0; i < units.length; i++) {
units[i].isSelected = false;
}
for (let i = 0; i < this.selectedUnits.length; i++) {
this.selectedUnits[i].isSelected = true;
}
};
play = () => {
Time.start();
window.addEventListener("mousedown", this.onMouseDown);
window.addEventListener("mousemove", this.onMouseMove);
window.addEventListener("mouseup", this.onMouseUp);
this.handleRequestFrame = window.requestAnimationFrame(this.onEnterFrame);
};
pause = () => {
if (this.handleRequestFrame === null) {
return;
}
window.cancelAnimationFrame(this.handleRequestFrame);
};
onEnterFrame = () => {
Time.update();
this.entityManager.update();
this.entityManager.render(this.context);
this.handleRequestFrame = window.requestAnimationFrame(this.onEnterFrame);
};
}
onMouseUp ํจ์์์ ์ ๋ ์ ํ ๋ก์ง์ ์ถ๊ฐํ์ต๋๋ค.
๋ฐ๋. ์ด์ ์ ํ์ด๋ฉ๋๋ค.
๋ง์ฐ์ค ์ผ์ชฝํด๋ฆญ์ผ๋ก ๋๋๊ทธ๋ฅผ ํ๊ณ ์ค๋ฅธ์ชฝ ํด๋ฆญ์ผ๋ก ์์ง์ด๊ฒ ํ๋ ค๋ฉด ์ผ๋จ onMouse* ํจ์์์
์ด๋ค ๋ฒํผ์ ๋๋ ๋์ง ๋๋์ด์ค์๋ค.
// App.ts
import Time from "./Time";
import EntityManager from "./EntityManager";
import Vector from "./Vector";
import Unit from "src/Unit";
export default class App {
static instance: App;
ref: HTMLElement;
canvas: HTMLCanvasElement;
context: CanvasRenderingContext2D;
handleRequestFrame: number | null = null;
entityManager: EntityManager;
isPressed: boolean = false;
mouseDownPosition: Vector = new Vector(0, 0);
mousePosition: Vector = new Vector(0, 0);
mouseUpPosition: Vector = new Vector(0, 0);
selectedUnits: Array<Unit> = [];
constructor(ref: HTMLElement) {
App.instance = this;
this.ref = ref;
this.canvas = document.createElement("canvas");
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
this.context = this.canvas.getContext("2d")!;
this.entityManager = new EntityManager();
this.ref.appendChild(this.canvas);
}
onMouseDown = (e: MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (e.button === 0) {
this.isPressed = true;
this.mouseDownPosition = new Vector(e.clientX, e.clientY);
} else if (e.button === 2) {
}
};
onMouseMove = (e: MouseEvent) => {
this.mousePosition = new Vector(e.clientX, e.clientY);
};
onMouseUp = (e: MouseEvent) => {
if (e.button !== 0) {
return;
}
this.isPressed = false;
this.mouseUpPosition = new Vector(e.clientX, e.clientY);
const startX = Math.min(this.mouseDownPosition.x, this.mouseUpPosition.x);
const endX = Math.max(this.mouseDownPosition.x, this.mouseUpPosition.x);
const startY = Math.min(this.mouseDownPosition.y, this.mouseUpPosition.y);
const endY = Math.max(this.mouseDownPosition.y, this.mouseUpPosition.y);
const units = this.entityManager.entities.filter(
(entity) => entity instanceof Unit
) as Unit[];
this.selectedUnits = units.filter((entity: Unit) => {
return (
entity.position.x >= startX &&
entity.position.x <= endX &&
entity.position.y >= startY &&
entity.position.y <= endY
);
});
for (let i = 0; i < units.length; i++) {
units[i].isSelected = false;
}
for (let i = 0; i < this.selectedUnits.length; i++) {
this.selectedUnits[i].isSelected = true;
}
};
play = () => {
Time.start();
window.addEventListener(
"contextmenu",
(e) => {
e.preventDefault();
},
false
);
window.addEventListener("mousedown", this.onMouseDown);
window.addEventListener("mousemove", this.onMouseMove);
window.addEventListener("mouseup", this.onMouseUp);
this.handleRequestFrame = window.requestAnimationFrame(this.onEnterFrame);
};
pause = () => {
if (this.handleRequestFrame === null) {
return;
}
window.cancelAnimationFrame(this.handleRequestFrame);
};
onEnterFrame = () => {
Time.update();
this.entityManager.update();
this.entityManager.render(this.context);
this.handleRequestFrame = window.requestAnimationFrame(this.onEnterFrame);
};
}
e.button์ผ๋ก ๋ง์ฐ์ค ์ผ์ชฝ ์ค๋ฅธ์ชฝ ๋ฒํผ์ ๊ตฌ๋ถํ ์์๊ณ
๊ทธ๋ฆฌ๊ณ contextmenu ์ด๋ฒคํธ๋ ๋นํ์ฑํ ํด์ค์ผํฉ๋๋ค
// Vector.ts
export default class Vector {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
public angleBetween(other: Vector): number {
return Math.atan2(other.y - this.y, other.x - this.x);
}
public distance(other: Vector): number {
return Math.sqrt(Math.pow(this.x - other.x, 2) + Math.pow(this.y - other.y, 2));
}
}
๋ชฉํ ์ง์ ๊ณผ ์ ๋์ ๋ ์ขํ์ฌ์ด์ ๊ฐ๋๋ฅผ ๊ตฌํ ํจ์์ ๊ฑฐ๋ฆฌ๋ฅผ ์ธก์ ํ ์ ์๋ ํจ์๋ฅผ ์ถ๊ฐํ๊ณ
// Unit.ts
import Entity from "./Entity";
import Vector from "./Vector";
import Time from './Time';
function drawEllipse(
context: CanvasRenderingContext2D,
x: number,
y: number,
w: number,
h: number
) {
const kappa = 0.5522848,
ox = (w / 2) * kappa,
oy = (h / 2) * kappa,
xe = x + w,
ye = y + h,
xm = x + w / 2,
ym = y + h / 2;
context.beginPath();
context.moveTo(x, ym);
context.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
context.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
context.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
context.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
context.closePath();
context.stroke();
}
export default class Unit extends Entity {
radius: number;
speed: number;
movement: boolean = false;
targetPosition: Vector = new Vector(0, 0);
isSelected: boolean = false;
constructor(position: Vector) {
super(position);
this.radius = 15;
this.speed = 100;
}
update() {
if (this.movement) {
const angle = this.position.angleBetween(this.targetPosition);
this.position.x += Math.cos(angle) * this.speed * Time.delta;
this.position.y += Math.sin(angle) * this.speed * Time.delta;
if (this.position.distance(this.targetPosition) <= 1) {
this.movement = false;
}
}
}
render(context: CanvasRenderingContext2D) {
if (this.isSelected) {
context.beginPath();
context.lineWidth = 2;
context.strokeStyle = "#0f0";
drawEllipse(
context,
this.position.x - this.radius,
this.position.y + this.radius * 0.75,
this.radius * 2,
this.radius * 0.75
);
}
context.beginPath();
context.fillStyle = "#000";
context.arc(this.position.x, this.position.y, this.radius, 0, Math.PI * 2);
context.fill();
}
}
ํด๋น ํจ์๋ค์ ์ด์ฉํด ์์ง์ด๋ผ๋ ๋ช ๋ น์ ๋ฐ์ผ๋ฉด ํด๋น ์ขํ๊น์ง ์์ง์ด๊ณ ๋์ฐฉํ๋ฉด ๋ฉ์ถ๋ ๋ก์ง์ ์ถ๊ฐํ์ต๋๋ค.
// App.ts
import Time from "./Time";
import EntityManager from "./EntityManager";
import Vector from "./Vector";
import Unit from "src/Unit";
export default class App {
static instance: App;
ref: HTMLElement;
canvas: HTMLCanvasElement;
context: CanvasRenderingContext2D;
handleRequestFrame: number | null = null;
entityManager: EntityManager;
isPressed: boolean = false;
mouseDownPosition: Vector = new Vector(0, 0);
mousePosition: Vector = new Vector(0, 0);
mouseUpPosition: Vector = new Vector(0, 0);
selectedUnits: Array<Unit> = [];
constructor(ref: HTMLElement) {
App.instance = this;
this.ref = ref;
this.canvas = document.createElement("canvas");
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
this.context = this.canvas.getContext("2d")!;
this.entityManager = new EntityManager();
this.ref.appendChild(this.canvas);
}
onMouseDown = (e: MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (e.button === 0) {
this.isPressed = true;
this.mouseDownPosition = new Vector(e.clientX, e.clientY);
} else if (e.button === 2) {
for (let i = 0; i < this.selectedUnits.length; i++) {
this.selectedUnits[i].movement = true;
this.selectedUnits[i].targetPosition = new Vector(e.clientX, e.clientY);
}
}
};
onMouseMove = (e: MouseEvent) => {
this.mousePosition = new Vector(e.clientX, e.clientY);
};
onMouseUp = (e: MouseEvent) => {
if (e.button !== 0) {
return;
}
this.isPressed = false;
this.mouseUpPosition = new Vector(e.clientX, e.clientY);
const startX = Math.min(this.mouseDownPosition.x, this.mouseUpPosition.x);
const endX = Math.max(this.mouseDownPosition.x, this.mouseUpPosition.x);
const startY = Math.min(this.mouseDownPosition.y, this.mouseUpPosition.y);
const endY = Math.max(this.mouseDownPosition.y, this.mouseUpPosition.y);
const units = this.entityManager.entities.filter(
(entity) => entity instanceof Unit
) as Unit[];
this.selectedUnits = units.filter((entity: Unit) => {
return (
entity.position.x >= startX &&
entity.position.x <= endX &&
entity.position.y >= startY &&
entity.position.y <= endY
);
});
for (let i = 0; i < units.length; i++) {
units[i].isSelected = false;
}
for (let i = 0; i < this.selectedUnits.length; i++) {
this.selectedUnits[i].isSelected = true;
}
};
play = () => {
Time.start();
window.addEventListener(
"contextmenu",
(e) => {
e.preventDefault();
},
false
);
window.addEventListener("mousedown", this.onMouseDown);
window.addEventListener("mousemove", this.onMouseMove);
window.addEventListener("mouseup", this.onMouseUp);
this.handleRequestFrame = window.requestAnimationFrame(this.onEnterFrame);
};
pause = () => {
if (this.handleRequestFrame === null) {
return;
}
window.cancelAnimationFrame(this.handleRequestFrame);
};
onEnterFrame = () => {
Time.update();
this.entityManager.update();
this.entityManager.render(this.context);
this.handleRequestFrame = window.requestAnimationFrame(this.onEnterFrame);
};
}
๋ง์ฐ์ค ์ค๋ฅธ์ชฝ ํด๋ฆญ์ด ๋๋ ธ์๋ ์ ํ๋ ์ ๋ ์ ์ฒด์๊ฒ ํด๋ฆญํ ์ขํ๋ก ์ด๋ํ๋ผ๊ณ ๊ฐ์ ๋ฃ์ด์ฃผ์์ต๋๋ค.
๋๋์ด ์์ง์ ๋๋ค. ํ์ง๋ง ์ ๋์ ์ฌ๋ฌ๊ฐ ์ถ๊ฐํด๋ณด๋๋ก ํ์ฃ .
์ ๋๋ผ๋ฆฌ ์๋ก ๊ฒน์น๊ฒ๋ฉ๋๋ค (ํ๋๋ผ ๊ฒน์น๊ธฐ).
ํด๋น ์ด์๋ฅผ ๊ณ ์ณ๋ณด๋๋ก ํฉ์๋ค.
// Unit.ts
import Entity from "./Entity";
import Vector from "./Vector";
import Time from './Time';
import EntityManager from 'src/EntityManager';
function drawEllipse(
context: CanvasRenderingContext2D,
x: number,
y: number,
w: number,
h: number
) {
const kappa = 0.5522848,
ox = (w / 2) * kappa,
oy = (h / 2) * kappa,
xe = x + w,
ye = y + h,
xm = x + w / 2,
ym = y + h / 2;
context.beginPath();
context.moveTo(x, ym);
context.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
context.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
context.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
context.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
context.closePath();
context.stroke();
}
export default class Unit extends Entity {
radius: number;
speed: number;
movement: boolean = false;
targetPosition: Vector = new Vector(0, 0);
isSelected: boolean = false;
constructor(position: Vector) {
super(position);
this.radius = 15;
this.speed = 100;
}
update() {
const units = EntityManager.instance.entities.filter((entity) => entity instanceof Unit) as Unit[];
for (let i = 0; i < units.length; i++) {
const unit = units[i];
if (this !== unit) {
const distance = this.position.distance(unit.position);
const length = this.radius + unit.radius;
if (distance <= length) {
const force = length - distance;
const angle = this.position.angleBetween(unit.position);
this.position.x -= Math.cos(angle) * force;
this.position.y -= Math.sin(angle) * force;
}
}
}
if (this.movement) {
const angle = this.position.angleBetween(this.targetPosition);
this.position.x += Math.cos(angle) * this.speed * Time.delta;
this.position.y += Math.sin(angle) * this.speed * Time.delta;
if (this.position.distance(this.targetPosition) <= 1) {
this.movement = false;
}
}
}
render(context: CanvasRenderingContext2D) {
if (this.isSelected) {
context.beginPath();
context.lineWidth = 2;
context.strokeStyle = "#0f0";
drawEllipse(
context,
this.position.x - this.radius,
this.position.y + this.radius * 0.75,
this.radius * 2,
this.radius * 0.75
);
}
context.beginPath();
context.fillStyle = "#000";
context.arc(this.position.x, this.position.y, this.radius, 0, Math.PI * 2);
context.fill();
}
}
EntityManager์์ Unit์ ๋ชจ๋ ๊ฐ์ ธ์ ์๊ธฐ ์์ ์ด ์๋ ๋ค๋ฅธ ์ ๋๊ณผ ์ถฉ๋ํ ๊ฒฝ์ฐ
์ถฉ๋ํ๋งํผ ๋ฐ๋ ค๋๊ฒ ์์ฑํ์ต๋๋ค.
์ ๋๋ผ๋ฆฌ ์ด์ ๊ฒน์น์ง๋ ์๋๋ฐ ์๋ก ์ถฉ๋ํ๋๊น ๋ชฉ์ ์ง๊ฐ ๊ฐ์์ ๊ณ์ ๋ชฉ์ ์ง์ ๋๋ฌํ๋ ค๊ณ ํ๋๊น
์์ง์์ด ๋ฉ์ถ์ง์๋ ์ด์๊ฐ ์๋ค์ ํด๋น ์ด์๋ ๋ค์ํธ์์ ํด๊ฒฐํ๋๋ก ํ์์ฃ .
๋ต ์ฌ๊ธฐ๊น์ง Canvas๋ก StarCraft๋ง๋ค์ด๋ณด๊ธฐ์์ต๋๋ค.
WE-AR์์ ์ฑ์ฉ์ ์งํํ๊ณ ์์ต๋๋ค.
https://www.notion.so/WE-AR-b8fc4563ede64311896e032aaada52d4
์ค ํฅ๋ฏธ๋กญ๊ฒ ์๋ดค์ต๋๋ค!