본문 바로가기
개발일지/GoodWishes

[굿위시 제작기] 6. 굿즈 상세 페이지 제작하기

by GiraffePark 2024. 9. 17.

 


굿즈의 상세한 정보를 보여주는 페이지를 만들자

저번에 만든 굿즈 메인페이지의 굿즈를 클릭하면, 그 굿즈에 대해서 정확하게 보여주는 페이지가 필요합니다.

지금부터 그 페이지를 만들겠습니다.

 

 

 

 


페이지 디자인

굿즈의 내부 정보랑, 같은 카테고리에 포함된 다른 굿즈들을 보여주는 것을 목표로 디자인을 했습니다.

 

이후 코딩과정에서 두 가지 변경사항이 생겼습니다.

1. 태그기능은 넣지 않고, 카테고리(분류)만 일단 넣자.

2. '같은 카테고리의 물건들' 항목은 앱의 안정성을 더 키운 이후에 업데이트하자

 

 

 

 


제작 결과물

아이폰 15프로를 기준으로 출력된 모습입니다.

 

 


페이지 소스코드

import 'package:flutter/material.dart';
import 'package:goodwishes/Models/goods_model.dart';
import 'package:goodwishes/widgets/goods/goods_detail_list.dart';
import 'package:goodwishes/widgets/goods/goods_detail_thumb.dart';
import 'package:goodwishes/widgets/goods/goods_detail_title.dart';
import 'package:provider/provider.dart';

class GoodsDetailPage extends StatelessWidget {
  final String id;

  const GoodsDetailPage({
    super.key,
    required this.id,
  });

  @override
  Widget build(BuildContext context) {
    Goods goods = context.watch<GoodsListProvider>().goodsList.firstWhere(
        (element) => element.id == id,
        orElse: () => Goods.createEmptyGoods());

    if (goods.id.isEmpty) {
      // Handle the case where the goods is not found
      return const Scaffold(
        body: Center(
          child: Text('Goods not found'),
        ),
      );
    }

    return Scaffold(
      appBar: AppBar(),
      body: SafeArea(
        child: SingleChildScrollView(
          scrollDirection: Axis.vertical,
          child: Column(
            children: [
              GoodsDetailThumb(
                goods: goods,
              ),
              GoodsDetailTitle(
                goods: goods,
              ),
              GoodsDetailList(
                goods: goods,
              ),
            ],
          ),
        ),
      ),
    );
  }
}

이제 페이지를 구성하는 내부 위젯들에 대해 차례대로 알아보겠습니다.

 

 


상단 : GoodDetailThumb

화면의 맨 상단에는, 굿즈의 썸네일이 보여지는 코드를 짰습니다.

 

 

 

import 'package:flutter/material.dart';
import 'package:goodwishes/Models/goods_model.dart';

class GoodsDetailThumb extends StatelessWidget {
  final Goods goods;

  const GoodsDetailThumb({
    super.key,
    required this.goods,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      height: MediaQuery.of(context).size.width,
      width: MediaQuery.of(context).size.width,
      decoration: BoxDecoration(
        image: DecorationImage(
          fit: BoxFit.cover,
          image: MemoryImage(goods.thumbnail),
        ),
      ),
    );
  }
}

사진의 크기는 1:1로 지정을 했고, 스마트폰 크기의 가로px를 기준으로 맞췄습니다.

굿즈의 썸네일은 Uint8List로 저장됐기 때문에, MemoryImage()를 이용해서 불러와야 합니다.

그리고 BoxDecoration의 BoxFit.cover를 이용해서, 자연스럽게 썸네일 이미지가 보여지게 끔 설정했습니다.

 

 

 

 


중단 : GoodDetailTitle

썸네일 밑에서, 굿즈의 대표 정보와 북마크 여부를 표시합니다.

 

1. 굿즈의 이름과 획득 날짜를 표시합니다.

2. 굿즈의 북마크 여부를 알려주고, 북마크 추가/제거 기능의 버튼 역할을 합니다.

3. 굿즈의 카테고리를 표시합니다.

 

 

 

import 'package:flutter/material.dart';
import 'package:goodwishes/Models/goods_model.dart';
import 'package:goodwishes/constants/ui_numbers.dart';
import 'package:goodwishes/widgets/tag.dart';
import 'package:provider/provider.dart';

class GoodsDetailTitle extends StatelessWidget {
  final Goods goods;

  const GoodsDetailTitle({
    super.key,
    required this.goods,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 150,
      width: MediaQuery.of(context).size.width,
      color: const Color(0xFFDBCACA),
      padding: const EdgeInsets.symmetric(
        horizontal: 17,
        vertical: 10,
      ),
      child: Column(
        children: [
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  const SizedBox(
                    height: 10,
                  ),
                  Text(
                    goods.goodsName,
                    style: const TextStyle(
                      fontSize: 18,
                    ),
                  ),
                  Text(
                    goods.date,
                    style: const TextStyle(
                      fontSize: 13,
                    ),
                  ),
                  const SizedBox(
                    height: UIDefault.sizedBoxHeight,
                  ),
                  Tag(
                    tagName: goods.category,
                    // onNavigate: () {},
                  ),
                ],
              ),
              IconButton(
                style: const ButtonStyle(
                    padding: WidgetStatePropertyAll(EdgeInsets.zero),
                    iconSize: WidgetStatePropertyAll(UIDefault.buttonSize)),
                icon: !goods.isFavorite
                    ? const Icon(Icons.bookmark_add_outlined)
                    : const Icon(Icons.bookmark_added_rounded),
                onPressed: () {
                  Provider.of<GoodsListProvider>(context, listen: false)
                      .updateIsFavorite(goods.id);
                },
              )
            ],
          ),
        ],
      ),
    );
  }
}

여기서 IconButton(즐겨찾기 버튼)만 좀 더 깊이 있게 보자면,

 

 

 

            IconButton(
                style: const ButtonStyle(
                    padding: WidgetStatePropertyAll(EdgeInsets.zero),
                    iconSize: WidgetStatePropertyAll(UIDefault.buttonSize)),
                icon: !goods.isFavorite
                    ? const Icon(Icons.bookmark_add_outlined)
                    : const Icon(Icons.bookmark_added_rounded),
                onPressed: () {
                  Provider.of<GoodsListProvider>(context, listen: false)
                      .updateIsFavorite(goods.id);
                },
              )

icon : 해당 굿즈의 isFavorite 값이 true/false일 때 상황에 맞춰서 적절하게 아이콘을 출력합니다.

onPressed: 버튼을 눌렀을 때, Provider를 이용해서 굿즈의 isFavorite 여부를 업데이트합니다.

 

 

 


하단 : GoodDetailList

import 'package:flutter/material.dart';
import 'package:goodwishes/Models/goods_model.dart';
import 'package:goodwishes/widgets/goods/goods_delete_button.dart';
import 'package:goodwishes/widgets/goods/goods_detail_list_el.dart';
import 'package:goodwishes/widgets/goods/goods_rewrite_button.dart';
import 'package:goodwishes/widgets/section_title.dart';

class GoodsDetailList extends StatelessWidget {
  final Goods goods;

  const GoodsDetailList({
    super.key,
    required this.goods,
  });

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 24.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const SizedBox(
            height: 30,
          ),
          GoodsDetailListEl(
            leftText: '소지 수량',
            rightText: goods.amount.toString(),
          ),
          GoodsDetailListEl(
            leftText: '구매 가격',
            rightText: goods.price.toString(),
          ),
          GoodsDetailListEl(
            leftText: '구매 방법',
            rightText: goods.wayToBuy,
          ),
          GoodsDetailListEl(
            leftText: '보관 장소',
            rightText: goods.location,
          ),
          const SizedBox(
            height: 5,
          ),
          const SectionTitle(titleText: '메모'),
          Container(
            height: 240,
            width: MediaQuery.of(context).size.width,
            padding: const EdgeInsets.all(17),
            decoration: const BoxDecoration(
              color: Color.fromARGB(255, 228, 228, 228),
              borderRadius: BorderRadius.all(
                Radius.circular(15),
              ),
            ),
            child: Text(goods.memo),
          ),
          const SizedBox(
            height: 30,
          ),
          GoodsRewriteButton(
            goods: goods,
            categoryName: goods.category,
          ),
          const SizedBox(
            height: 15,
          ),
          GoodsDeleteButton(
            id: goods.id,
            categoryName: goods.category,
          ),
        ],
      ),
    );
  }
}

 

 

GoodsDetailListEl() 위젯이 많은데,

굿즈의 상세정보들을 출력해주는 역할을 담당합니다.

 

 

 

import 'package:flutter/material.dart';

class GoodsDetailListEl extends StatelessWidget {
  final String leftText;
  final String rightText;

  const GoodsDetailListEl({
    super.key,
    required this.leftText,
    required this.rightText,
  });

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Row(
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            Text(
              leftText,
              style: const TextStyle(
                fontWeight: FontWeight.bold,
                fontSize: 20,
              ),
            ),
            const SizedBox(
              width: 35,
            ),
            Text(
              rightText,
              style: const TextStyle(
                fontSize: 19,
              ),
            ),
          ],
        ),
        const SizedBox(
          height: 22,
        ),
      ],
    );
  }
}

 

 

 

 


최하단의 버튼들

굿즈 상세페이지를 맨 밑으로 내리면, 수정/삭제 버튼이 있습니다.

 

 

하단의 GoodsDetailList()위젯 코드의 맨 밑에,

          GoodsRewriteButton(
            goods: goods,
            categoryName: goods.category,
          ),
          const SizedBox(
            height: 15,
          ),
          GoodsDeleteButton(
            id: goods.id,
            categoryName: goods.category,
          ),

GoodsDeleteButton, GoodsRewriteButton이 있습니다.

 

 

GoodsRewriteButton

import 'package:flutter/material.dart';

import 'package:goodwishes/Models/goods_model.dart';
import 'package:goodwishes/pages/goods_rewrite_page.dart';

class GoodsRewriteButton extends StatelessWidget {
  final Goods goods;
  final String categoryName;
  const GoodsRewriteButton({
    super.key,
    required this.goods,
    required this.categoryName,
  });

  @override
  Widget build(BuildContext context) {
    return TextButton(
      style:
          const ButtonStyle(padding: WidgetStatePropertyAll(EdgeInsets.zero)),
      onPressed: () {
        Navigator.push(
          context,
          MaterialPageRoute(
            builder: (context) => GoodsRewritePage(goods: goods),
          ),
        );
      },
      child: Container(
        height: 50,
        decoration: const BoxDecoration(
          color: Color.fromARGB(255, 224, 224, 224),
          borderRadius: BorderRadius.all(
            Radius.circular(5),
          ),
        ),
        child: const Center(
          child: Text(
            '수정',
            style: TextStyle(
              fontWeight: FontWeight.bold,
              fontSize: 18,
            ),
          ),
        ),
      ),
    );
  }
}

수정 버튼을 누르면, 굿즈를 수정하는 'GoodsRewritePage'로 이동합니다.

 

 

GoodsDeleteButton

import 'package:flutter/material.dart';
import 'package:goodwishes/Functions/show_info_dialog.dart';
import 'package:goodwishes/Models/category_model.dart';
import 'package:goodwishes/Models/goods_model.dart';
import 'package:provider/provider.dart';

class GoodsDeleteButton extends StatelessWidget {
  final String id;
  final String categoryName;
  const GoodsDeleteButton({
    super.key,
    required this.id,
    required this.categoryName,
  });

  @override
  Widget build(BuildContext context) {
    return TextButton(
      style:
          const ButtonStyle(padding: WidgetStatePropertyAll(EdgeInsets.zero)),
      onPressed: () {
        Provider.of<GoodsListProvider>(context, listen: false).removeGoods(id);
        Provider.of<CategoryListProvider>(context, listen: false)
            .downCountCategory(categoryName);
        if (context.mounted) {
          Navigator.pop(context);
          showInfoDialog(
            context,
            '알림',
            '굿즈가 제거되었습니다.',
          );
        }
      },
      child: Container(
        height: 50,
        decoration: const BoxDecoration(
          color: Color(0xFFDBCACA),
          borderRadius: BorderRadius.all(
            Radius.circular(5),
          ),
        ),
        child: const Center(
          child: Text(
            '삭제',
            style: TextStyle(
              fontWeight: FontWeight.bold,
              fontSize: 18,
            ),
          ),
        ),
      ),
    );
  }
}

GoodsDeleteButton을 누르면, GoodsListProvider와 연동하여 굿즈를 지우고, CategoryListProvider와 연동하여 해당 굿즈가 들어간 카테고리의 굿즈 개수를 -1 합니다.

 

 

 

 

	if (context.mounted) {
          Navigator.pop(context);
          showInfoDialog(
            context,
            '알림',
            '굿즈가 제거되었습니다.',
          );
        }

그리고 굿즈 제거가 끝나면, '굿즈가 제거되었습니다.' 알림을 표시하면서

굿즈 상세페이지를 빠져나옵니다.

 

 

반응형