[Nest.js] point 개발
포인트를 도입 한 계기
우리는 분기당 한번씩 매거진을 발행을 하게 되었는데, 첫 번째 매거진을 발행을 했을 때 반응이 너무나 좋았다. 그래서 매거진을 돈으로 판매를 하는 것 보단 포인트로 판매를 하는 것이 어떻나 라는 의견이 나와서 포인트를 생각을 하게 되었다. 또한 하루 평균 100명의 유저의 유입이 발생이 되는데, 조금 더 활동을 할 수 있게끔 하기 위해서 이벤트를 통해 포인트를 지급 할 수 있게끔 하는 것이 목표였다.
포인트 기획
포인트를 얼마를 줄 것인가, 어떤 이벤트에는 몇 포인트, 어떤 상품은 몇 포인트 등등 의 기획은 내가 아닌 다른 팀원들이 맡아서 진행이 되었다.
포인트 테이블 설계
나는 이 포인트를 지급을 하고, 가장 중요한 선입 선출법으로 포인트를 사용을 할 수 있게끔 하는 것이 가장 중요했다. 그리고 아무래도 결제에서도 포인트를 쓸 수 있어야 함으로 더욱 신중하게 고민을 해봤다.
고려해 봐야 하는 점
포인트 유효기간
포인트는 지급을 받은 이후 1년이 지나면 자동으로 소멸이 되도록 해야 한다.
포인트 선입 선출
포인트를 제일 먼저 받았던, 유효 기간이 지나지 않은 포인트를 먼저 사용하도록 해야 한다.
포인트가 딱 떨어 지지 않는 경우
포인트를 만약 1000 포인트를 받았는데, 만약 500 포인트만 사용을 했다면, 남은 500 포인트를 나중에 또 가장 먼저 쓰게 해야 한다.
targetPointId
지급받은 한 포인트를 사용을 한 뒤 포인트가 남았을 때를 고려 해봤을 때, 처음 지급받은 포인트의 Pk 값을 fk로 가지고 있는 targetPointId가 필요할 것이라고 생각을 했다. 그래야지 나중에 포인트를 사용을 했을 때, targetPointId로 group by를 한 뒤, sum(point)를 한다면 각 지급받은 포인트 row 마다 얼마나 남았는지 계산이 가능하다고 생각을 했다.
예를 들어 이런식으로 포인트를 지급 받고 사용을 했다고 했을 때,
select
p.targetPointId,
sum(p.point)
from
point p
where
p.userId = 1247
group by
p.targetPointId
이런식으로 지급 받은 포인트를 얼마나 썼는지에 대해 알아 볼 수 있게끔 하면 어떨까 라고 생각을 했었다.
포인트 targetPointId trigger 생성
일단 targetPointId를 trigger로 생성이 되게끔 하기로 했다. point 테이블에 insert이 되면 자동으로 trigger이 작동 하도록 하였다. targetPointId 지정을 해주는 function을 만들었다. 코드로는 이렇다.
CREATE OR REPLACE FUNCTION public.insert_point_table_targetpointid()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
begin
if new."targetPointId" is null then
update "point" p
set
"targetPointId" = new."id"
where
p.id = new."id";
end if;
return null;
end;
$function$
;
여기서 new는 새롭게 insert 된 point row 값이다. 새롭게 insert 된 point의 pk 값을 targetPointId로 지정이 되도록 update를 하는 function 이다.
포인트 사용
해당 포인트를 얼마나 사용을 했고, 얼마나 남았는지에 대한 것들은 세팅이 되었다. 이제는 포인트를 선입선출 방법으로 사용이 되게끔 해야 한다. 우선 이 방법을 사용하기 위해서 function을 만들었다.
CREATE OR REPLACE FUNCTION public.use_point(userid integer, point integer)
RETURNS integer[]
LANGUAGE plpgsql
AS $function$
declare exist_point integer:= point;
declare min_point integer;
declare temprow record;
declare point_ids integer Array;
declare inserted_id integer;
begin for temprow in
select
p.*, p2."remainPoint"
from point p
left join lateral (
select
sum(p2."point") as "remainPoint"
from
point p2
where
p2."targetPointId" = p."id"
) p2 on true
where
p."type" = 'add' and
p2."remainPoint" > 0 and
p."userId" = userId
order by p."expiredAt" asc
loop
if exist_point > 0 then
min_point := least(temprow."remainPoint", exist_point);
insert into "point"
(
"userId", "type", "point", "targetPointId", "expiredAt"
)
values
(
userId, 'use', min_point * -1, temprow.id, temprow."expiredAt"
) returning id into inserted_id;
exist_point := exist_point - min_point;
point_ids := array_append(point_ids, inserted_id);
else
exit;
end if;
end loop;
return point_ids;
end
$function$
;
우선 function에 대해 차근차근 알아보겠다.
CREATE OR REPLACE FUNCTION public.use_point(userid integer, point integer)
RETURNS integer[]
LANGUAGE plpgsql
AS $function$
declare exist_point integer:= point;
declare min_point integer;
declare temprow record;
declare point_ids integer Array;
declare inserted_id integer;
우선 function의 인자로 userId와 point를 받는다. 여기서 point는 사용할 포인트를 의미한다. declare를 사용하면 변수를 정의를 하는 것과 같이 사용할 수 있다.
begin for temprow in
select
p.*, p2."remainPoint"
from point p
left join lateral (
select
sum(p2."point") as "remainPoint"
from
point p2
where
p2."targetPointId" = p."id"
) p2 on true
where
p."type" = 'add' and
p2."remainPoint" > 0 and
p."userId" = userId
order by p."expiredAt" asc
다음은 select 문이 나오는데, 이는 위에서 targetPointId로 group by를 하여 각 포인트가 얼마나 남았는지에 대해 알려주는 용도로 사용한다. 남은 포인트를 remainPoint로 alias를 하였고, 이 row 테이블을 temprow의 변수에 담았다. order by로 선입선출을 할 수 있도록 정렬을 하였다.
loop
if exist_point > 0 then
min_point := least(temprow."remainPoint", exist_point);
insert into "point"
(
"userId", "type", "point", "targetPointId", "expiredAt"
)
values
(
userId, 'use', min_point * -1, temprow.id, temprow."expiredAt"
) returning id into inserted_id;
exist_point := exist_point - min_point;
point_ids := array_append(point_ids, inserted_id);
else
exit;
end if;
end loop;
여기서는 loop를 통한 for문을 돌렸다. exist_point는 사용할 포인트로 0 포인트 이상일 때 if문이 돌도록 하였다. 여기서 temprow에 가장 첫 번째 요소를 시작으로 remainPoint와 exist_point와의 least로 비교를 하여 min_point에 담아준다. 이것은 remainPoint에 min_point 만큼 차감을 시키기 위한 것이다.
그래서 차감한 포인트 만큼 insert 문을 통해 point 테이블에 반영을 해준다. 그리고 point_ids를 반환을 하는데, 이는 trade 테이블에 담기게 된다. 그래서 나중에 환불을 했을 때, 어떤 포인트를 환불을 해줘야 할 지 알아야 하므로 이것을 반환을 하였다.
마치며
point를 사용할 때 아무렇지 않게 사용을 했었지만 이렇게나 복잡한 로직을 갖고 있는지 몰랐다. 특히 결제와 관련된 부분이라 결제 로직과 포인트 사용 로직과 함께 고려를 해야 하는 것이 힘들었다.
댓글남기기