Scrollable Widgets 메모

Baek Dong Hyun·2023년 1월 5일
1

SingleChildScrollView

1. 기본 렌더링 법

class SingleChildScrollViewScreen extends StatelessWidget {
  final List<int> numbers = List.generate(100, (index) => index);

  SingleChildScrollViewScreen({super.key});

  
  Widget build(BuildContext context) {
    return MainLayout(
      title: 'SingleChildScrollView', 
      body: renderSimple()
    );
  }

  // 1. 기본 렌더링법
  Widget renderSimple() {
    return SingleChildScrollView(
      child: Column(
        children: rainbowColors.map((e) => renderContainer(color: e)).toList()
      ),
    );
  }

  Widget renderContainer({required Color color}) {
    return Container(
      height: 300,
      color: color,
    );
  }
}

2. 화면을 넘어가지 않더라도 스크롤 되게

import 'package:flutter/material.dart';
import 'package:scrollable_widgets/constants/colors.dart';
import 'package:scrollable_widgets/layouts/main_layout.dart';

class SingleChildScrollViewScreen extends StatelessWidget {
  final List<int> numbers = List.generate(100, (index) => index);

  SingleChildScrollViewScreen({super.key});

  
  Widget build(BuildContext context) {
    return MainLayout(
      title: 'SingleChildScrollView', 
      body: renderAlwaysScroll()
    );
  }

  // 2. 화면을 넘어가지 않더라도 스크롤 되게
  Widget renderAlwaysScroll() {
    return SingleChildScrollView(
      // 기본값
      // physics: NeverScrollableScrollPhysics(),
      // 넘어가지 않더라도 스크롤 되게
      physics: AlwaysScrollableScrollPhysics(),
      child: Column(
        children: [
          renderContainer(color: Colors.pink)
        ]
      ),
    );
  }

  Widget renderContainer({required Color color}) {
    return Container(
      height: 300,
      color: color,
    );
  }
}

3. 잘리지 않고 스크롤되게

import 'package:flutter/material.dart';
import 'package:scrollable_widgets/constants/colors.dart';
import 'package:scrollable_widgets/layouts/main_layout.dart';

class SingleChildScrollViewScreen extends StatelessWidget {
  final List<int> numbers = List.generate(100, (index) => index);

  SingleChildScrollViewScreen({super.key});

  
  Widget build(BuildContext context) {
    return MainLayout(
      title: 'SingleChildScrollView', 
      body: renderClip()
    );
  }

  // 3. 잘리지 않고 스크롤되게
  Widget renderClip() {
    return SingleChildScrollView(
      // 넘어가지 않더라도 스크롤 되게
      clipBehavior: Clip.none,
      child: Column(
        children: [
          renderContainer(color: Colors.pink)
        ]
      ),
    );
  }

  Widget renderContainer({required Color color}) {
    return Container(
      height: 300,
      color: color,
    );
  }
}

4. 여러가지 physics 정리

import 'package:flutter/material.dart';
import 'package:scrollable_widgets/constants/colors.dart';
import 'package:scrollable_widgets/layouts/main_layout.dart';

class SingleChildScrollViewScreen extends StatelessWidget {
  final List<int> numbers = List.generate(100, (index) => index);

  SingleChildScrollViewScreen({super.key});

  
  Widget build(BuildContext context) {
    return MainLayout(
      title: 'SingleChildScrollView', 
      body: renderPhysics()
    );
  }

  // 4. 여러가지 physics 정리
  Widget renderPhysics() {
    return SingleChildScrollView(
      // 기본값 NeverScrollableScrollPhysics() -> 넘어갈게 없다면 스크롤 안됨
      // AlwaysScrollableScrollPhysics() -> 넘어갈게 없더라도 스크롤 되게
      // BouncingScrollPhysics() -> 원래 iOS에서만 스크롤이 튕기는데 이거 넣으면 안드로이드도 튕김
      // ClampingScrollPhysics() -> 안드로이드처럼 스크롤 안튕기게
      physics: ClampingScrollPhysics(),
      child: Column(
        children: rainbowColors.map((e) => renderContainer(color: e)).toList()
      ),
    );
  }

  Widget renderContainer({required Color color}) {
    return Container(
      height: 300,
      color: color,
    );
  }
}

5. singleChildScrollView 퍼포먼스

그렇게 썩 좋지는 않다.

100개의 Container를 렌더링한다면 ListView 같은경우는 한번에 다 불러오지않고 끊어서 효율적으로 불러오는 반면 100개를 한번에 싹다 렌더링해버린다.

ListView

1. 기본적인 렌더링

얘도 SingleChildScrollView 처럼 한번에 다 나옴

import 'package:flutter/material.dart';
import 'package:scrollable_widgets/constants/colors.dart';
import 'package:scrollable_widgets/layouts/main_layout.dart';

class ListViewScreen extends StatelessWidget {
  final List<int> numbers = List.generate(100, (index) => index);

  ListViewScreen({super.key});

  
  Widget build(BuildContext context) {
    return MainLayout(
      title: 'ListView', 
      body: renderDefault()
    );
  }

  Widget renderDefault() {
    return ListView(
      children: numbers.map((e) => 
        renderContainer(
          color: rainbowColors[e % rainbowColors.length], 
          index: e + 1
        )
      ).toList()
    );
  }

  Widget renderContainer({required Color color, required int index}) {
    return Container(
      height: 300,
      color: color,
      child: Center(
        child: Text(
          '$index 번째 리스트', 
          style: const TextStyle(
            fontSize: 30.0, 
            fontWeight: FontWeight.w700, 
            color: Colors.white
          )
        )
      )
    );
  }
}

2. ListView.builder

보여지는 부분만 렌더링 되고 그 후 스크롤 할 때 맞춰서 더 렌더링 해줌 ( 더 효율적임 )

import 'package:flutter/material.dart';
import 'package:scrollable_widgets/constants/colors.dart';
import 'package:scrollable_widgets/layouts/main_layout.dart';

class ListViewScreen extends StatelessWidget {
  final List<int> numbers = List.generate(100, (index) => index);

  ListViewScreen({super.key});

  
  Widget build(BuildContext context) {
    return MainLayout(
      title: 'ListView', 
      body: renderBuilder()
    );
  }

  Widget renderBuilder() {
    return ListView.builder(
      itemCount: 100,
      itemBuilder: (BuildContext context, int index) {
        print('지금 몇번째게요? : $index 번입니다~!');
        return renderContainer(
          color: rainbowColors[index % rainbowColors.length], 
          index: index + 1
        );
      }
    );
  }

  Widget renderContainer({required Color color, required int index}) {
    return Container(
      height: 300,
      color: color,
      child: Center(
        child: Text(
          '$index 번째 리스트', 
          style: const TextStyle(
            fontSize: 30.0, 
            fontWeight: FontWeight.w700, 
            color: Colors.white
          )
        )
      )
    );
  }
}

3. ListView.separated

보여지는 부분만 렌더링 되고 그 후 스크롤 할 때 맞춰서 더 렌더링 해줌 + 중간 중간에 추가할 위젯을 넣을 수 있음

import 'package:flutter/material.dart';
import 'package:scrollable_widgets/constants/colors.dart';
import 'package:scrollable_widgets/layouts/main_layout.dart';

class ListViewScreen extends StatelessWidget {
  final List<int> numbers = List.generate(100, (index) => index);

  ListViewScreen({super.key});

  
  Widget build(BuildContext context) {
    return MainLayout(
      title: 'ListView', 
      body: renderSeparated()
    );
  }

  // 3. 2. + 중간 중간에 추가할 위젯을 넣을 수 있음
  Widget renderSeparated() {
    return ListView.separated(
      itemCount: 100,
      separatorBuilder: (BuildContext context, int index) { 
        if (index % 5 != 0 || index == 0) return Container();
        
        print('separatorBuilder 몇번째게요? : $index 번입니다~!');
        return renderContainer(
          color: Colors.black, 
          index: index,
          height: 100.0
        );
      },
      itemBuilder: (BuildContext context, int index) {
        print('지금 몇번째게요? : $index 번입니다~!');
        return renderContainer(
          color: rainbowColors[index % rainbowColors.length], 
          index: index + 1
        );
      }
    );
  }

  Widget renderContainer({required Color color, required int index, double? height}) {
    return Container(
      height: height ?? 300.0,
      color: color,
      child: Center(
        child: Text(
          '$index 번째 리스트', 
          style: const TextStyle(
            fontSize: 30.0, 
            fontWeight: FontWeight.w700, 
            color: Colors.white
          )
        )
      )
    );
  }
}

GridView

1. 기본적인 렌더링

import 'package:flutter/material.dart';
import 'package:scrollable_widgets/constants/colors.dart';
import 'package:scrollable_widgets/layouts/main_layout.dart';

class GridViewScreen extends StatelessWidget {
  List<int> numbers = List.generate(100, (index) => index);

  GridViewScreen({super.key});

  
  Widget build(BuildContext context) {
    return MainLayout(
      title: 'GridViewScreen',
      body: renderCount()
    );
  }

  // 1. 한번에 다 그림
  Widget renderCount() {
    return GridView.count(
      // 가로로 몇개 넣을래?
      crossAxisCount: 2,
      // 간격
      crossAxisSpacing: 12.0,
      mainAxisSpacing: 12.0,
      children: numbers.map((e) => renderContainer(color: rainbowColors[e % rainbowColors.length], index: e)).toList(),
    );
  }

  Widget renderContainer({required Color color, required int index, double? height}) {
    print('내가 지금 몇번째이게? $index');
    return Container(
      height: height ?? 300.0,
      color: color,
      child: Center(
        child: Text(
          '$index 번째 리스트', 
          style: const TextStyle(
            fontSize: 30.0, 
            fontWeight: FontWeight.w700, 
            color: Colors.white
          )
        )
      )
    );
  }
}

2. GridView.builder

보여지는 부분만 렌더링 되고 그 후 스크롤 할 때 맞춰서 더 렌더링 해줌 ( 더 효율적임 )

    1. 기본적인 SliverGridDelegateWithFixedCrossAxisCount()
import 'package:flutter/material.dart';
import 'package:scrollable_widgets/constants/colors.dart';
import 'package:scrollable_widgets/layouts/main_layout.dart';

class GridViewScreen extends StatelessWidget {
  List<int> numbers = List.generate(100, (index) => index);

  GridViewScreen({super.key});

  
  Widget build(BuildContext context) {
    return MainLayout(
      title: 'GridViewScreen',
      body: renderBuilder()
    );
  }

  // 2. 보이는 것만 그림
  Widget renderBuilder() {
    return GridView.builder(
      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 2,
        crossAxisSpacing: 12.0,
        mainAxisSpacing: 12.0,
      ),
      // 아이템 총 갯수
      itemCount: 100,
      itemBuilder: (context, index) {
        return renderContainer(color: rainbowColors[index % rainbowColors.length], index: index);
      }
    );
  }

  Widget renderContainer({required Color color, required int index, double? height}) {
    print('내가 지금 몇번째이게? $index');
    return Container(
      height: height ?? 300.0,
      color: color,
      child: Center(
        child: Text(
          '$index 번째 리스트', 
          style: const TextStyle(
            fontSize: 30.0, 
            fontWeight: FontWeight.w700, 
            color: Colors.white
          )
        )
      )
    );
  }
}
  • 2.최대 사이즈 SliverGridDelegateWithMaxCrossAxisExtent()
import 'package:flutter/material.dart';
import 'package:scrollable_widgets/constants/colors.dart';
import 'package:scrollable_widgets/layouts/main_layout.dart';

class GridViewScreen extends StatelessWidget {
  List<int> numbers = List.generate(100, (index) => index);

  GridViewScreen({super.key});

  
  Widget build(BuildContext context) {
    return MainLayout(
      title: 'GridViewScreen',
      body: renderBuilderCrossAxisCount()
    );
  }

  // 3. 최대 사이즈
  Widget renderBuilderCrossAxisCount() {
    return GridView.builder(
			// 스크롤을 가로로 
      scrollDirection: Axis.horizontal,
      gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
        // 길이
        maxCrossAxisExtent: 100
      ),
      itemCount: 100,
      itemBuilder: (context, index) {
        return renderContainer(color: rainbowColors[index % rainbowColors.length], index: index);
      }
    );
  }

  Widget renderContainer({required Color color, required int index, double? height}) {
    print('내가 지금 몇번째이게? $index');
    return Container(
      height: height ?? 300.0,
      color: color,
      child: Center(
        child: Text(
          '$index 번째 리스트', 
          style: const TextStyle(
            fontSize: 30.0, 
            fontWeight: FontWeight.w700, 
            color: Colors.white
          )
        )
      )
    );
  }
}

CustomScrollView

1. SliverAppBar

import 'package:flutter/material.dart';
import 'package:scrollable_widgets/constants/colors.dart';

class CustomScrollViewScreen extends StatelessWidget {
  List<int> numbers = List.generate(100, (index) => index);
  CustomScrollViewScreen({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        slivers: [
          renderSliverAppBar(),
          renderHeader(),
          renderBuilderSliverList(),
          renderHeader(),
          renderSliverGridBuilder(),
          renderHeader(),
          renderBuilderSliverList(),
        ],
      )
    );
  }

  // AppBar
  SliverAppBar renderSliverAppBar() {
    return const SliverAppBar(
      // 밑으로 스크롤하면 안보이고, 위로 스크롤하면 나오는거 floating: true (기본값은 false)
      floating: true,
      // 상단 고정 pinned: true (기본값은 false)
      pinned: false,
      // 자석효과 : 진짜 살짝만 아래로 내리거나 올리면 앱바가 올라가거나 내려감 (이거 쓸라면 floating true , pinned false)
      snap: true,
      // app bar를 따라오게 (기본값은 false 근데) 원래는 뒤에 흰 배경이 보임
      stretch: true,
      
      // app bar height
      expandedHeight: 200,
      // 텍스트가 app bar가 사라지지않았는데 더 빨리 사라짐
      collapsedHeight: 150,

      flexibleSpace: FlexibleSpaceBar(
        // 배경을 넣을 수 있음 (위로 스크롤하면 자연스레 사라짐)
        background: Image(image: AssetImage('asset/image_1.jpeg'), fit: BoxFit.cover),
        // 앱바 밑에 title에 적은 글씨가 나옴
        title: Text('FlexibleSpace'),
      ),
      title: Text('custom scroll view'),
    );
  }

  Widget renderContainer({required Color color, required int index, double? height}) {
    print('내가 지금 몇번째이게? $index');
    return Container(
      height: height ?? 300.0,
      color: color,
      child: Center(
        child: Text(
          '$index 번째 리스트', 
          style: const TextStyle(
            fontSize: 30.0, 
            fontWeight: FontWeight.w700, 
            color: Colors.white
          )
        )
      )
    );
  }
}

2. SliverList - SliverChildListDelegate

ListView 기본 생성자와 유사함

SliverList renderChildSliverList() {
  return SliverList(
    delegate: SliverChildListDelegate(
      numbers.map((e) => renderContainer(color: rainbowColors[e % rainbowColors.length], index: e)).toList()
    ),
  );
}

3. SliverList - SliverChildBuilderDelegate

ListView.builder 와 유사함

SliverList renderBuilderSliverList() {
  return SliverList(
    delegate: SliverChildBuilderDelegate(
      (context, index) {
        return renderContainer(color: rainbowColors[index % rainbowColors.length], index: index);
      },
      childCount: 10
    ),
  );
}

4. SliverGrid - SliverChildListDelegate

GridView.count 와 유사함

SliverGrid renderChildSliverGrid() {
  return SliverGrid(
    delegate: SliverChildListDelegate(
      numbers.map((e) => renderContainer(color: rainbowColors[e % rainbowColors.length], index: 1)).toList()
    ), 
    // max 최대 넓이 정하고 그 넓이 안에서 균등하게 배분
    // fix 가로로 몇개 할건지
    gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
      crossAxisCount: 2
    )
  );
}

5. SliverGrid - SliverChildBuilderDelegate

GridView.builder 와 유사함

SliverGrid renderSliverGridBuilder() {
    return SliverGrid(
      delegate: SliverChildBuilderDelegate(
        (context, index) {
          return renderContainer(color: rainbowColors[index % rainbowColors.length], index: index);
        }
      ),
      gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
        maxCrossAxisExtent: 150,
      ),
    );
  }

6. SliverPersistentHeader - sticky같은 느낌?

class _SliverFixedHeaderDelegate extends SliverPersistentHeaderDelegate {
  final Widget child;
  final double maxHeight;
  final double minHeight;

  _SliverFixedHeaderDelegate({required this.child, required this.maxHeight, required this.minHeight});

  
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
    // TODO: implement build
    return SizedBox.expand(child: child);
  }

  
  // TODO: implement maxExtent -> 최대 높이
  double get maxExtent => maxHeight;

  
  // TODO: implement minExtent -> 최소 높이
  double get minExtent => minHeight;

  // covariant -> 상속된 클래스도 사용가능
  // oldDelegate -> build가 실행이 됐을 때 이전 Delegate
  // this -> 새로운 delegate
  // shouldRebuild -> 새로 build를 해야할지 말지 결정
  // false -> build 안함 true -> build 다시함
  
  bool shouldRebuild(_SliverFixedHeaderDelegate oldDelegate) {
    // TODO: implement shouldRebuild
    return oldDelegate.minHeight != minHeight || oldDelegate.maxHeight != maxHeight || oldDelegate.child != child;
    // minHeight 나 maxHeight 나 child가 바꼈을 때 build 다시 함
  }

}

class CustomScrollViewScreen extends StatelessWidget {
  List<int> numbers = List.generate(100, (index) => index);
  CustomScrollViewScreen({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        slivers: [
          renderSliverAppBar(),
          renderHeader(),
          renderBuilderSliverList(),
          renderHeader(),
          renderSliverGridBuilder(),
          renderHeader(),
          renderBuilderSliverList(),
        ],
      )
    );
  }

	SliverPersistentHeader renderHeader() {
	  return SliverPersistentHeader(
	    pinned: true,
	    delegate: _SliverFixedHeaderDelegate(
	      child: Container(
	        color: Colors.black,
	        child: const Center(
	          child: Text(
	            '신기하지~',
	            style: TextStyle(
	              color: Colors.white,
	            ),
	          ),
	        ),
	      ),
	      minHeight: 50,
	      maxHeight: 200,
	    ),
	  );
	}

	Widget renderContainer({required Color color, required int index, double? height}) {
    print('내가 지금 몇번째이게? $index');
    return Container(
      height: height ?? 300.0,
      color: color,
      child: Center(
        child: Text(
          '$index 번째 리스트', 
          style: const TextStyle(
            fontSize: 30.0, 
            fontWeight: FontWeight.w700, 
            color: Colors.white
          )
        )
      )
    );
  }
}

ScrollbarScreen

웹과 마찬가지로 우측에 스크롤바가 생김

import 'package:flutter/material.dart';
import 'package:scrollable_widgets/constants/colors.dart';
import 'package:scrollable_widgets/layouts/main_layout.dart';

class ScrollbarScreen extends StatelessWidget {
  List<int> numbers = List.generate(100, (index) => index);

  ScrollbarScreen({super.key});

  
  Widget build(BuildContext context) {
    return MainLayout(
      title: 'ScrollbarScreen',
      body: Scrollbar(
        child: SingleChildScrollView(
          child: Column(
            children: numbers.map((e) => renderContainer(color: rainbowColors[e % rainbowColors.length], index: e)).toList(),
          ),
        ),
      ),
    );
  }

  Widget renderContainer({required Color color, required int index, double? height}) {
    print('내가 지금 몇번째이게? $index');
    return Container(
      height: height ?? 300.0,
      color: color,
      child: Center(
        child: Text(
          '$index 번째 리스트', 
          style: const TextStyle(
            fontSize: 30.0, 
            fontWeight: FontWeight.w700, 
            color: Colors.white
          )
        )
      )
    );
  }
}

ReorderableListView

1,2,3…순서로 나열된 리스트를 중간에 터치를 길게해 위치를 바꿀 수 있다. 4,2,1,5,3… 이런 식으로

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:scrollable_widgets/constants/colors.dart';
import 'package:scrollable_widgets/layouts/main_layout.dart';

class ReorderableListViewScreen extends StatefulWidget {
  const ReorderableListViewScreen({super.key});

  
  State<ReorderableListViewScreen> createState() => _ReorderableListViewScreenState();
}

class _ReorderableListViewScreenState extends State<ReorderableListViewScreen> {
  List<int> numbers = List.generate(100, (index) => index);

  
  Widget build(BuildContext context) {
    return MainLayout(
      title: 'ReorderableListViewScreen', 
      body: renderBuilder()
    );
  }

  Widget renderDefault() {
    return ReorderableListView(
      onReorder: ((int oldIndex, int newIndex) {
        setState(() {
          // oldIndex와 newIndex 모두 이동이 되기 전에 산정한다. 

          // [red, orange, yellow]
          // [0, 1, 2]
          // red 를 yellow 다음으로 옮기고 싶음
          // red : 0 oldIndex -> 3 newIndex
          // [orange , yellow , red]

          if (oldIndex < newIndex) {
            newIndex -= 1;
          }

          // [red , orange , yellow]
          // yellow를 맨 앞으로 옮기고 싶다.
          // yellow : 2 oldIndex -> 0 newIndex
          // [yellow, red, orange]

          final item = numbers.removeAt(oldIndex);
          numbers.insert(newIndex, item);
        });
      }),
      children: numbers.map((e) => renderContainer(color: rainbowColors[e % rainbowColors.length], index: e)).toList()
    );
  }

  Widget renderBuilder() {
    return ReorderableListView.builder(
      itemCount: 100, 
      onReorder: ((int oldIndex, int newIndex) {
        setState(() {
          // oldIndex와 newIndex 모두 이동이 되기 전에 산정한다. 

          // [red, orange, yellow]
          // [0, 1, 2]
          // red 를 yellow 다음으로 옮기고 싶음
          // red : 0 oldIndex -> 3 newIndex
          // [orange , yellow , red]

          if (oldIndex < newIndex) {
            newIndex -= 1;
          }

          // [red , orange , yellow]
          // yellow를 맨 앞으로 옮기고 싶다.
          // yellow : 2 oldIndex -> 0 newIndex
          // [yellow, red, orange]

          final item = numbers.removeAt(oldIndex);
          numbers.insert(newIndex, item);
        });
      }),
      itemBuilder: ((context, index) {
        return renderContainer(color: rainbowColors[numbers[index] % rainbowColors.length], index: numbers[index]);
      }), 
    );
  }

  Widget renderContainer({required Color color, required int index, double? height}) {
    print('내가 지금 몇번째이게? $index');
    return Container(
      key: Key(index.toString()),
      height: height ?? 300.0,
      color: color,
      child: Center(
        child: Text(
          '$index 번째 리스트', 
          style: const TextStyle(
            fontSize: 30.0, 
            fontWeight: FontWeight.w700, 
            color: Colors.white
          )
        )
      )
    );
  }
}

RefreshIndicator

서버 요청을 해야할 때 위에서 아래로 스크롤을 당기면 새로고침되면서 다시 서버요청을 할 수 있다.

import 'package:flutter/material.dart';
import 'package:scrollable_widgets/constants/colors.dart';
import 'package:scrollable_widgets/layouts/main_layout.dart';

class RefreshIndicatorScreen extends StatelessWidget {
  List<int> numbers = List.generate(100, (index) => index);

  RefreshIndicatorScreen({super.key});

  
  Widget build(BuildContext context) {
    return MainLayout(
      title: 'RefreshIndicator', 
      body: RefreshIndicator(
        onRefresh: () async {
          // 서버 요쳥
          await Future.delayed(Duration(seconds: 1));
        },
        child: ListView(
          children: numbers.map((e) {
            return renderContainer(
              color: rainbowColors[e % rainbowColors.length], 
              index: e + 1
            );
          }).toList()
        ),
      )
    );
  }
  

  Widget renderContainer({required Color color, required int index, double? height}) {
    print('내가 지금 몇번째이게? $index');
    return Container(
      height: height ?? 300.0,
      color: color,
      child: Center(
        child: Text(
          '$index 번째 리스트', 
          style: const TextStyle(
            fontSize: 30.0, 
            fontWeight: FontWeight.w700, 
            color: Colors.white
          )
        )
      )
    );
  }
}
profile
안녕하세요.

0개의 댓글