Phoenix Contexts Guide의 이해 #3
context 가이드 정리의 마지막이다. 여기서는 order 관련한 코드들을 추가한다. 다음 그림은 이번 포스트에서 생성할 테이블과 스키마, 컨텍스트 및 컨트롤러 모듈들을 나타낸다.
스키마 및 테이블 생성
generator로 주문 관련 코드를 생성한다. LineItem
및 Order
스키마 모두 Orders
컨텍스트를 사용할 것이므로 LineItem
은 mix phx.gen.context
generator를 사용한다.
$ mix phx.gen.html Orders Order orders user_uuid:uuid total_price:decimal
$ mix phx.gen.context Orders LineItem order_line_items \
price:decimal \
quantity:integer \
order_id:references:orders \
product_id:references:products
마이그레이션을 실행하기 전에 DB에 적용할 스키마 모듈을 일부 수정한다. lib/hello/orders/order.ex
파일을 열어 다음과 같이 has_many
코드를 추가한다. Order와 LineItem은 1:n의 관계를 가지게 되고, Order와 Product는 LineItem을 통해서 1:n 관계를 가진다.
field :user_uuid, Ecto.UUID
+ has_many :line_items, Hello.Orders.LineItem
+ has_many :products, through: [:line_items, :product]
timestamps()
그리고 lib/hello/orders/line_item.ex
파일을 열어 다음과 같이 수정한다. LineItem 입장에서는 Order와 belongs_to
관계가 맞지만, Product하고도 belongs_to
관계를 명시해야 하는 지는 고민이 필요한 부분이다.
field :quantity, :integer
- field :order_id, :id
- field :product_id, :id
+
+ belongs_to :order, Hello.Orders.Order
+ belongs_to :product, Hello.Catalog.Product
timestamps()
OrderController
컨트롤러도 같이 만들어졌을테니 여기로 연결하는 route를 lib/hello_web/router.ex
에 추가한다.
put "/cart", CartController, :update
+ resources "/orders", OrderController, only: [:create, :show]
end
마이그레이션을 진행한다.
$ mix ecto.migrate
context 모듈의 수정
lib/hello/orders.ex
파일의 get_order!/1
을 사용자 ID도 함께 받도록 인수를 확장하고 상품 정보도 함께 로드할 수 있도록 수정한다.
- def get_order!(id), do: Repo.get!(Order, id)
+ def get_order!(user_uuid, id) do
+ Order
+ |> Repo.get_by!(id: id, user_uuid: user_uuid)
+ |> Repo.preload([line_items: [:product]])
+ end
같은 모듈에서 complete_order/1
함수를 새로 추가한다.
alias Hello.ShoppingCart
alias Hello.Orders.LineItem
def complete_order(%ShoppingCart.Cart{} = cart) do
line_items =
Enum.map(cart.items, fn item ->
%{product_id: item.product_id, price: item.product.price, quantity: item.quantity}
end)
order =
Ecto.Changeset.change(%Order{},
user_uuid: cart.user_uuid,
total_price: ShoppingCart.total_cart_price(cart),
line_items: line_items
)
Ecto.Multi.new()
|> Ecto.Multi.insert(:order, order)
|> Ecto.Multi.run(:prune_cart, fn _repo, _changes ->
ShoppingCart.prune_cart_items(cart)
end)
|> Repo.transaction()
|> case do
{:ok, %{order: order}} -> {:ok, order}
{:error, name, value, _changes_so_far} -> {:error, {name, value}}
end
end
controller 모듈의 수정
이제 이 수정한 함수들을 OrderController
모듈에 적용한다. lib/hello_web/controllers/order_controller.ex
파일에서 먼저 create/2
함수를 다음과 같이 수정한다.
- def create(conn, %{"order" => order_params}) do
- case Orders.create_order(order_params) do
+ def create(conn, _) do
+ case Orders.complete_order(conn.assigns.cart) do
{:ok, order} ->
conn
|> put_flash(:info, "Order created successfully.")
|> redirect(to: ~p"/orders/#{order}")
+ {:error, _reason} ->
+ conn
+ |> put_flash(:error, "There was an error processing your order")
+ |> redirect(to: ~p"/cart")
- {:error, %Ecto.Changeset{} = changeset} ->
- render(conn, :new, changeset: changeset)
end
end
show/2
함수에서는 get_order!/1
함수를 사용하고 있으니 이를 수정한다.
def show(conn, %{"id" => id}) do
- order = Orders.get_order!(id)
+ order = Orders.get_order!(conn.assigns.current_uuid, id)
render(conn, :show, order: order)
end
이외에도 edit/2
, update/2
, delete/2
에서도 get_order!/1
함수를 사용하고 있지만, 위에서 추가한 route 코드에서도 볼 수 있듯이 여기 예제에서는 사용하지 않는다. 그대로 둬도 되고 찜찜하면 삭제한다.
또한 complete_order/1
에서 호출하는 함수 중 ShoppingCart.prune_cart_items/1
도 새로 추가된 함수이니 lib/hello/shopping_cart.ex
파일에 추가한다.
def prune_cart_items(%Cart{} = cart) do
{_, _} = Repo.delete_all(from(i in CartItem, where: i.cart_id == ^cart.id))
{:ok, reload_cart(cart)}
end
HTML view 수정
lib/hello_web/controllers/order_html.ex
파일에서 볼 수 있는 것처럼, lib/hello_web/controllers/order_html/
밑에 .heex
템플릿 파일들을 읽어들여 뷰로 사용한다.
defmodule HelloWeb.OrderHTML do
use HelloWeb, :html
embed_templates "order_html/*"
...
end
lib/hello_web/controllers/order_html/show.html.heex
파일을 아래와 같이 작성한다. 가이드 문서에서와 달리 기존에 생성된 .heex 파일을 기반으로 수정한 것인데, 브라우저 상에서도 보기 좋다 (...)
<.header>
Thank you for your order!
<:subtitle>User ID: <%= @order.user_uuid %></:subtitle>
</.header>
<.table id="line_items" rows={@order.line_items}>
<:col :let={item} label="Title"><%= item.product.title %></:col>
<:col :let={item} label="Quantity"><%= item.quantity %></:col>
<:col :let={item} label="Price"><%= HelloWeb.CartHTML.currency_to_str(item.price) %></:col>
</.table>
<strong>Total Price:</strong>
<%= HelloWeb.CartHTML.currency_to_str(@order.total_price) %>
<.back navigate={~p"/cart"}>Back</.back>
그리고 lib/hello_web/controllers/cart_html/show.html.heex
파일에 위 UI를 호출할 코드를 추가한다.
+ <.link href={~p"/orders"} method="post">complete order</.link>
<% end %>
http://localhost:4000/products
UI에서 상품을 카트에 담으면 아래와 같은 UI가 나타날 것이다. 수량도 적당히 업데이트해본다.
여기서 'Complete Order'를 누르면 아래와 같이 `Thank you for your odrder!' 화면이 나오는 것을 볼 수 있다.
이로써 가이드에 있는 내용은 대부분 다루었다.
마무리
세 부분에 걸쳐 phoenix의 contexts 가이드를 내 나름대로 이해한 내용을 설명했다. 사실 아직 나는 급한 경사도의 학습 곡선 중에서 거의 바닥에 있는 편이다.
모든 내용을 이해한건 아니다. many-to-many나 one-to-many 연관관계를 만들기 위해 수정한 부분도 좀더 이해가 필요한 상황이고, 언제 mix phnx.gen.html
을 사용해야 할 지, 대신 mix phx.gen.context
를 사용해야 할 지 판단하는 방법도 파악해야 한다. .heex 템플릿 작성 방법도 언젠간 숙지해야 한다. 지금까지 포스팅한 내용 중에 갱신해야 할 내용에 대해서는 추가 포스팅을 하도록 하겠다.