본문 바로가기

코딩강의/인스타그램클론(expo-노마드코더)

Upload Photo

🍔 핵심 내용

 

🥑 사진 업로드 부분을 만들기 위해서는 아래 큰 두가지 작업이 필요 하다

 

1) 현재 세팅이 탭 네비게이션 -> 스택 네비게이션으로 되어 있는데, 이를
스택 네비게이션 -> 탭 네비게이션 -> 스택 네비게이션 개념으로 해줄 것이다.

 

말로 설명하기 어렵긴 하지만, 이유는 하단 카메라 탭을 눌렀을 경우  

 

 

https://reactnavigation.org/docs/material-top-tab-navigator/ 들어가서

npm install @react-navigation/material-top-tabs react-native-tab-view@^2.16.0 설치

 

2) 카메라 화면에서는 2개의 탭이 존재하는데, 이는 meterial top tabs를 이용하였다.

 

🍔 코드 리뷰

 

🥑 LoggedInNav.js

import React from "react";
import { createStackNavigator } from "@react-navigation/stack";
import TabsNav from "./TabsNav";
import UploadNav from "./UploadNav";

const Stack = createStackNavigator();

export default function LoggedInNav() {
  return (
    <Stack.Navigator headerMode="none" mode="modal">
      <Stack.Screen name="Tabs" component={TabsNav} />
      <Stack.Screen name="Upload" component={UploadNav} />
    </Stack.Navigator>
  );
}

로그인 후, 2개의 스택이 존재한다. 그리고 처음에 TabNav로 가는 스택이 쓰인다.

TabNav로 가게 되면... 아래 이어서

 

🥑 TabNav.js

import React from "react";
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import { Image, View } from "react-native";
import TabIcon from "../components/nav/TabIcon";
import SharedStackNav from "./SharedStackNav";
import useMe from "../hooks/useMe";

const Tabs = createBottomTabNavigator();

export default function TabsNav() {
  const { data } = useMe();
  return (
    <Tabs.Navigator
      tabBarOptions={{
        activeTintColor: "white",
        showLabel: false,
        style: {
          borderTopColor: "rgba(255, 255, 255, 0.3)",
          backgroundColor: "black",
        },
      }}
    >
      <Tabs.Screen
        name="Feed"
        options={{
          tabBarIcon: ({ focused, color, size }) => (
            <TabIcon iconName={"home"} color={color} focused={focused} />
          ),
        }}
      >
        {() => <SharedStackNav screenName="Feed" />}
      </Tabs.Screen>
      <Tabs.Screen
        name="Search"
        options={{
          tabBarIcon: ({ focused, color, size }) => (
            <TabIcon iconName={"search"} color={color} focused={focused} />
          ),
        }}
      >
        {() => <SharedStackNav screenName="Search" />}
      </Tabs.Screen>
      <Tabs.Screen
        name="Camera"
        component={View}
        listeners={({ navigation }) => {
          return {
            tabPress: (e) => {
              e.preventDefault();
              navigation.navigate("Upload");
            },
          };
        }}
        options={{
          tabBarIcon: ({ focused, color, size }) => (
            <TabIcon iconName={"camera"} color={color} focused={focused} />
          ),
        }}
      />
      <Tabs.Screen
        name="Notifications"
        options={{
          tabBarIcon: ({ focused, color, size }) => (
            <TabIcon iconName={"heart"} color={color} focused={focused} />
          ),
        }}
      >
        {() => <SharedStackNav screenName="Notifications" />}
      </Tabs.Screen>
      <Tabs.Screen
        name="Me"
        options={{
          tabBarIcon: ({ focused, color, size }) =>
            data?.me?.avatar ? (
              <Image
                source={{ uri: data.me.avatar }}
                style={{
                  height: 20,
                  width: 20,
                  borderRadius: 10,
                  ...(focused && { borderColor: "white", borderWidth: 1 }),
                }}
              />
            ) : (
              <TabIcon iconName={"person"} color={color} focused={focused} />
            ),
        }}
      >
        {() => <SharedStackNav screenName="Me" />}
      </Tabs.Screen>
    </Tabs.Navigator>
  );
}

이전에 만들었던 바텀 탭이 초기 화면에 쓰이게 되고, 여기서 카메라 탭을 누르면 초기에 만들었던 UploadNav 화면으로 이동하게 된다.

 

🥑 UploadNav.js

import React from "react";
import { createMaterialTopTabNavigator } from "@react-navigation/material-top-tabs";
import SelectPhoto from "../screens/SelectPhoto";
import TakePhoto from "../screens/TakePhoto";
import { createStackNavigator } from "@react-navigation/stack";

const Tab = createMaterialTopTabNavigator();
const Stack = createStackNavigator();

export default function UploadNav() {
  return (
    <Tab.Navigator
      tabBarPosition="bottom"
      tabBarOptions={{
        style: {
          backgroundColor: "black",
        },
        activeTintColor: "white",
        indicatorStyle: {
          backgroundColor: "white",
          top: 0,
        },
      }}
    >
      <Tab.Screen name="Select">
        {() => (
          <Stack.Navigator>
            <Stack.Screen name="Select" component={SelectPhoto} />
          </Stack.Navigator>
        )}
      </Tab.Screen>
      <Tab.Screen name="Take" component={TakePhoto} />
    </Tab.Navigator>
  );
}

UploadNav에는 2개의 스크린이 존재 하는데,

 

첫번째 Select 스크린에는 Stack이 쓰인다. 그 이유는 상단에 헤더를 넣기 위해서다. (아래 사진 참조)

 

두 번째 TAKE는 스택이 아니기 때문에 헤더가 없다.

 

지금 까지가 이 app의 기초 뼈대라고 볼 수 있다.


🍔 핵심 내용

 

🥑 헤더 뒤로 가기 버튼 이미지로 변경

 

🥑 SelectPhoto화면 꾸미기

 

SelectPhoto.js 화면에는 많은 중요한 내용들이 들어 간다

 

1) 앨범 접근 권한 요청

2) 모바일 앨범에서 사진 불러 오기

3) 사진 나열하기

등 등 중요한 내용들이 많이 있으니 하나 씩 살펴보자.

 

expo install expo-media-library 설치

 

🍔 코드 리뷰

 

🥑 UploadNav.js

import React from "react";
import { createMaterialTopTabNavigator } from "@react-navigation/material-top-tabs";
import SelectPhoto from "../screens/SelectPhoto";
import TakePhoto from "../screens/TakePhoto";
import { createStackNavigator, HeaderTitle } from "@react-navigation/stack";
import { Ionicons } from "@expo/vector-icons";

const Tab = createMaterialTopTabNavigator();
const Stack = createStackNavigator();

export default function UploadNav() {
  return (
    <Tab.Navigator
      tabBarPosition="bottom"
      tabBarOptions={{
        style: {
          backgroundColor: "black",
        },
        activeTintColor: "white",
        indicatorStyle: {
          backgroundColor: "white",
          top: 0,
        },
      }}
    >
      <Tab.Screen name="Select">
        {() => (
          <Stack.Navigator
            screenOptions={{
              headerTintColor: "white",
              headerBackTitleVisible: false,
              headerBackImage: ({ tintColor }) => (
                <Ionicons color={tintColor} name="close" size={28} />
              ),
              headerStyle: {
                backgroundColor: "black",
                shadowOpacity: 0.3,
              },
            }}
          >
            <Stack.Screen
              name="Select"
              component={SelectPhoto}
              options={{
                title: "Choose a photo",
              }}
            />
          </Stack.Navigator>
        )}
      </Tab.Screen>
      <Tab.Screen name="Take" component={TakePhoto} />
    </Tab.Navigator>
  );
}

1) headerBackImage에서 Ionicons를 불러와서 이미지를 바꿔주었다.

 

 

🥑 SelectPhoto.js

import React, { useEffect, useState } from "react";
import { Ionicons } from "@expo/vector-icons";
import * as MediaLibrary from "expo-media-library";
import styled from "styled-components/native";
import {
  FlatList,
  Image,
  TouchableOpacity,
  useWindowDimensions,
} from "react-native";
import { colors } from "../colors";

const Container = styled.View`
  flex: 1;
  background-color: black;
`;

const Top = styled.View`
  flex: 1;
  background-color: black;
`;

const Bottom = styled.View`
  flex: 1;
  background-color: black;
`;

const ImageContainer = styled.TouchableOpacity``;
const IconContainer = styled.View`
  position: absolute;
  bottom: 5px;
  right: 1px;
`;

const HeaderRightText = styled.Text`
  color: ${colors.blue};
  font-size: 16px;
  font-weight: 600;
  margin-right: 7px;
`;

export default function SelectPhoto({ navigation }) {
  const [photos, setPhotos] = useState([]);
  const [chosenPhoto, setChosenPhoto] = useState("");
  const getPhotos = async () => {
    const { assets: photos } = await MediaLibrary.getAssetsAsync({
      first: 52,
      sortBy: "creationTime",
    });
    setPhotos(photos);
    setChosenPhoto(photos[0]?.uri);
  };
  const getPermissions = async () => {
    const { status, canAskAgain } = await MediaLibrary.getPermissionsAsync();
    console.log(status, canAskAgain);
    if (status !== "granted" && canAskAgain) {
      const { status } = await MediaLibrary.requestPermissionsAsync();
      if (status !== "undetermined") {
        getPhotos();
      }
    } else if (status !== "undetermined") {
      getPhotos();
    }
  };
  const HeaderRight = () => (
    <TouchableOpacity>
      <HeaderRightText>Next</HeaderRightText>
    </TouchableOpacity>
  );
  useEffect(() => {
    getPermissions();
  }, []);
  useEffect(() => {
    navigation.setOptions({
      headerRight: HeaderRight,
    });
  }, []);
  const numColumns = 4;
  const { width } = useWindowDimensions();
  const choosePhoto = (uri) => {
    setChosenPhoto(uri);
  };
  const renderItem = ({ item: photo }) => (
    <ImageContainer onPress={() => choosePhoto(photo.uri)}>
      <Image
        source={{ uri: photo.uri }}
        style={{ width: width / numColumns, height: 100 }}
      />
      <IconContainer>
        <Ionicons
          name="checkmark-circle"
          size={18}
          color={photo.uri === chosenPhoto ? colors.blue : "white"}
        />
      </IconContainer>
    </ImageContainer>
  );
  return (
    <Container>
      <Top>
        {chosenPhoto !== "" ? (
          <Image
            source={{ uri: chosenPhoto }}
            style={{ width, height: "100%" }}
          />
        ) : null}
      </Top>
      <Bottom>
        <FlatList
          data={photos}
          numColumns={numColumns}
          keyExtractor={(photo) => photo.id}
          renderItem={renderItem}
        />
      </Bottom>
    </Container>
  );
}

 

Top 화면과 Bottom 화면을 꾸며 줄 것이다. Top 화면은 선택한 사진이 나오게 (초기 값은 맨 처음 사진), Bottom화면은 사진 리스트가 쭈루룩 나오게 만들어 줄 것이다.

 

1) Android와 IOS와 getPermissionsAsyn()의 결과 값이 다르다. Android는 위와 같이 status를 이용하면 된다. IOS같은 경우는 accessPrivileges 를 이용하면 될듯.

 

먼저 getPermissionsAsyn을 통해 현재 권한 요청 확인을 해본댜. 그리고 아직 권한이 없는 상태라면 권한을 request하는 함수를 발동한다. 권한 요청이 완료 되었으면 status 상태가 바뀌는데 이 값에 따라 허용이 되었으면 getPhotos 함수를 시행 하자

 

2) getPhotos 함수는 getAssetsAsync를 이용하여 assest이라는 결과 값을 받는다. 이는 expo에서 앨범에 있는 사진들을 모두 가지고 오는 기능이다.이때 여러 옵션들이 있는데, 위에 내용은 52개만, 그리고 생성날짜에 따라 내림차순으로 세팅해놓은 것이다. 이 외에도 여러 옵션들이 있는 것 같다. 해당 assets : photos 값은 state 배열 값에 담아 둔다. 그리고 ChosenPhoto의 첫 값으로 배열의 0번째 값을 넣어 둔다. (아래 top 화면 초기 화면을 위해)

 

이제 값들이 세팅 되었으니 화면에 뿌려주자

 

3) Top 화면에는 chosenPhoto가 빈값이 아니라면 Image uri 값에 chosenPhoto 값을 넣어주자. 초기 값은 처음 사진이고, 그리고 Bottom 화면에서 setChosenPhoto를 사용해주기 때문에 (onClick에서)  터치한 사진이 Top 화면에 보여줄 것이다.

 

4) Bottom 화면에서는 FlatList를 이용하여 data를 뿌려줄 것인데, data는 getPhotos를 통해 photos state에 담겨있다.

그리고 photo.uri === chosenPhoto를 이용해 내가 지금 누른 사진이 chosenPhoto값과 같으면 ionicons를 이용해 체크버튼을 꾸며 줄 수 있다. (나머지 내용들은 이전에 FlatList에서 다루었다. 그리고 사진이 너무 많으면 이전에 feed에서 이용했던 화면 아래로 넘어가면 추가 query하는걸 이용하던가 아니면 getAssetsAsync 자체 옵션중에 하나가 있을 수도 있다. (아래 결과 값보면, assets가 아닌 다른 hasNextPage같은 값을 통해서 추가 fetch 할 수 있을 거 같음.

 

 

5) 끝으로 헤더 오른쪽 부분에 Next 버튼을 만들어주었다. 해당 Next버튼을 누르면 upload라는 form이 나올 것이다. 이 내용은 추 후에 이어서,,


🍔 핵심 내용

 

🥑 TakePhoto 화면 꾸미기

 

1) 사용자가 직접 사진을 찍는 기능을 만들어 보자. 

 

expo install expo-camera 설치

npm install @react-native-community/slider 설치

 

 

🍔 코드 리뷰

 

🥑 TakePhoto.js

import { Camera } from "expo-camera";
import React, { useEffect, useRef, useState } from "react";
import { Ionicons } from "@expo/vector-icons";
import { Alert, Image, StatusBar, Text, TouchableOpacity } from "react-native";
import Slider from "@react-native-community/slider";
import styled from "styled-components/native";
import * as MediaLibrary from "expo-media-library";

const Container = styled.View`
  flex: 1;
  background-color: black;
`;

const Actions = styled.View`
  flex: 0.35;
  padding: 0px 50px;
  align-items: center;
  justify-content: space-around;
`;

const ButtonsContainer = styled.View`
  width: 100%;
  flex-direction: row;
  justify-content: space-between;
  align-items: center;
`;

const TakePhotoBtn = styled.TouchableOpacity`
  width: 100px;
  height: 100px;
  background-color: rgba(255, 255, 255, 0.5);
  border: 2px solid rgba(255, 255, 255, 0.8);
  border-radius: 50px;
`;

const SliderContainer = styled.View``;
const ActionsContainer = styled.View`
  flex-direction: row;
`;

const CloseButton = styled.TouchableOpacity`
  position: absolute;
  top: 20px;
  left: 20px;
`;

const PhotoActions = styled(Actions)`
  flex-direction: row;
`;

const PhotoAction = styled.TouchableOpacity`
  background-color: white;
  padding: 10px 25px;
  border-radius: 4px;
`;
const PhotoActionText = styled.Text`
  font-weight: 600;
`;

export default function TakePhoto({ navigation }) {
  const camera = useRef();
  const [takenPhoto, setTakenPhoto] = useState("");
  const [cameraReady, setCameraReady] = useState(false);
  const [ok, setOk] = useState(false);
  const [flashMode, setFlashMode] = useState(Camera.Constants.FlashMode.off);
  const [zoom, setZoom] = useState(0);
  const [cameraType, setCameraType] = useState(Camera.Constants.Type.front);
  const getPermissions = async () => {
    const { granted } = await Camera.requestPermissionsAsync();
    setOk(granted);
  };
  useEffect(() => {
    getPermissions();
  }, []);
  const onCameraSwitch = () => {
    if (cameraType === Camera.Constants.Type.front) {
      setCameraType(Camera.Constants.Type.back);
    } else {
      setCameraType(Camera.Constants.Type.front);
    }
  };
  const onZoomValueChange = (e) => {
    setZoom(e);
  };
  const onFlashChange = () => {
    if (flashMode === Camera.Constants.FlashMode.off) {
      setFlashMode(Camera.Constants.FlashMode.on);
    } else if (flashMode === Camera.Constants.FlashMode.on) {
      setFlashMode(Camera.Constants.FlashMode.auto);
    } else if (flashMode === Camera.Constants.FlashMode.auto) {
      setFlashMode(Camera.Constants.FlashMode.off);
    }
  };
  const goToUpload = async (save) => {
    if (save) {
      await MediaLibrary.saveToLibraryAsync(takenPhoto);
    }
    console.log("Will upload", takenPhoto);
  };
  const onUpload = () => {
    Alert.alert("Save photo?", "Save photo & upload or just upload", [
      {
        text: "Save & Upload",
        onPress: () => goToUpload(true),
      },
      {
        text: "Just Upload",
        onPress: () => goToUpload(false),
      },
    ]);
  };
  const onCameraReady = () => setCameraReady(true);
  const takePhoto = async () => {
    if (camera.current && cameraReady) {
      const { uri } = await camera.current.takePictureAsync({
        quality: 1,
        exif: true,
      });
      setTakenPhoto(uri);
    }
  };
  const onDismiss = () => setTakenPhoto("");
  return (
    <Container>
      <StatusBar hidden={true} />
      {takenPhoto === "" ? (
        <Camera
          type={cameraType}
          style={{ flex: 1 }}
          zoom={zoom}
          flashMode={flashMode}
          ref={camera}
          onCameraReady={onCameraReady}
        >
          <CloseButton onPress={() => navigation.navigate("Tabs")}>
            <Ionicons name="close" color="white" size={30} />
          </CloseButton>
        </Camera>
      ) : (
        <Image source={{ uri: takenPhoto }} style={{ flex: 1 }} />
      )}
      {takenPhoto === "" ? (
        <Actions>
          <SliderContainer>
            <Slider
              style={{ width: 200, height: 20 }}
              value={zoom}
              minimumValue={0}
              maximumValue={1}
              minimumTrackTintColor="#FFFFFF"
              maximumTrackTintColor="rgba(255, 255, 255, 0.5)"
              onValueChange={onZoomValueChange}
            />
          </SliderContainer>
          <ButtonsContainer>
            <TakePhotoBtn onPress={takePhoto} />
            <ActionsContainer>
              <TouchableOpacity
                onPress={onFlashChange}
                style={{ marginRight: 30 }}
              >
                <Ionicons
                  size={30}
                  color="white"
                  name={
                    flashMode === Camera.Constants.FlashMode.off
                      ? "flash-off"
                      : flashMode === Camera.Constants.FlashMode.on
                      ? "flash"
                      : flashMode === Camera.Constants.FlashMode.auto
                      ? "eye"
                      : ""
                  }
                />
              </TouchableOpacity>
              <TouchableOpacity onPress={onCameraSwitch}>
                <Ionicons
                  size={30}
                  color="white"
                  name={
                    cameraType === Camera.Constants.Type.front
                      ? "camera-reverse"
                      : "camera"
                  }
                />
              </TouchableOpacity>
            </ActionsContainer>
          </ButtonsContainer>
        </Actions>
      ) : (
        <PhotoActions>
          <PhotoAction onPress={onDismiss}>
            <PhotoActionText>Dismiss</PhotoActionText>
          </PhotoAction>
          <PhotoAction onPress={onUpload}>
            <PhotoActionText>Upload</PhotoActionText>
          </PhotoAction>
        </PhotoActions>
      )}
    </Container>
  );
}

1) takePhoto에는 아주 많은 기능들이 있다. 먼저 여기서 눈여겨 보아야 할 부분은 Alert로 2가지 선택 사항에 따라 각기 다른 함수를 호출 할 수 있다. 이 부분은 나중에 분명 쓰일 곳이 있을 것 같다.

 

2) useRef를 통해 화면 버튼과 camera 를 연결해 주었고, slider도 봐줄만하다. 

 

사실 내용이 워낙 많기 때문에 하나씩 다 설명 하기 보다는 핵심 내용부분만 스윽 눈으로 훑어서 봐보자.


🍔 핵심 내용

 

🥑 UploadForm 만들기

 

1) SelectPhoto나 TakePhoto에서 사진을 선택 한 후에, UploadForm으로 이동 한다.

UploadForm을 Mutation을 통해 마무리 해보자.

 

2) 제출 할 때 에러가 뜨는 에러를 잡아 보자.

 

npm i apollo-upload-client 설치 (업로드 관련 모듈)

 

🍔 코드 리뷰

 

🥑 apollo.js

...
const onErrorLink = onError(({ graphQLErrors, networkError }) => {
  if (graphQLErrors) {
    console.log(`GraphQL Error`, graphQLErrors);
  }
  if (networkError) {
    console.log("Network Error", networkError);
  }
});

export const cache = new InMemoryCache({
  typePolicies: {
    Query: {
      fields: {
        seeFeed: offsetLimitPagination(),
      },
    },
  },
});

const client = new ApolloClient({
  link: authLink.concat(onErrorLink).concat(httpLink),
  cache,
});
export default client;

위와 같이 진행하면 error에 대해 어떤 링크에서 발생한 에러인지 그 내역을 확인 할 수 있다. (단순 네트워크 에러인지, 백엔드 서버에서의 에러인지)

 

위 에러를 확인해보면, 백엔드에서의 에러임을 확인 할 수 있었고, 에러 내용은 파일 업로드 할때 문제 였다.

 

🥑 fragments.js

...
export const FEED_PHOTO = gql`
  fragment FeedPhoto on Photo {
    ...PhotoFragment
    user {
      id
      username
      avatar
    }
    caption
    createdAt
    isMine
  }
  ${PHOTO_FRAGMENT}
`;

uploadForm에 쓰일 프래그먼트 이다.

 

🥑 apollo.js

import {
  ApolloClient,
  createHttpLink,
  InMemoryCache,
  makeVar,
} from "@apollo/client";
import { onError } from "@apollo/client/link/error";
import { setContext } from "@apollo/client/link/context";
import { offsetLimitPagination } from "@apollo/client/utilities";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { createUploadLink } from "apollo-upload-client";

export const isLoggedInVar = makeVar(false);
export const tokenVar = makeVar("");

const TOKEN = "token";

export const logUserIn = async (token) => {
  await AsyncStorage.setItem(TOKEN, token);
  isLoggedInVar(true);
  tokenVar(token);
};

export const logUserOut = async () => {
  await AsyncStorage.removeItem(TOKEN);
  isLoggedInVar(false);
  tokenVar(null);
};

const uploadHttpLink = createUploadLink({
  uri: "https://683302505ed8.ngrok.io/graphql",
});

const authLink = setContext((_, { headers }) => {
  return {
    headers: {
      ...headers,
      token: tokenVar(),
    },
  };
});

const onErrorLink = onError(({ graphQLErrors, networkError }) => {
  if (graphQLErrors) {
    console.log(`GraphQL Error`, graphQLErrors);
  }
  if (networkError) {
    console.log("Network Error", networkError);
  }
});

export const cache = new InMemoryCache({
  typePolicies: {
    Query: {
      fields: {
        seeFeed: offsetLimitPagination(),
      },
    },
  },
});

const client = new ApolloClient({
  link: authLink.concat(onErrorLink).concat(uploadHttpLink),
  cache,
});
export default client;

1) ApolloClient 부분 마지막 httpLink 대신에 createUploadLink(apollo-client-upload 모듈)로 대체하였다. 업로드를 위해서는 해달 모듈을 사용해야 한다.

 

** 자꾸 네트워크 에러가 떠서 확인해보니 백엔드 node 버전이 12이하가 되어야 upload 하는데 문제가 없는거,,, node 12로 다운 시켰다. (nvm으로 노드 버전 조종 가능)

 

🥑 UploadForm.js

import { gql, useMutation } from "@apollo/client";
import { ReactNativeFile } from "apollo-upload-client";
import React, { useEffect } from "react";
import { useForm } from "react-hook-form";
import { ActivityIndicator, Text, View } from "react-native";
import { TouchableOpacity } from "react-native-gesture-handler";
import styled from "styled-components/native";
import { colors } from "../colors";
import DismissKeyboard from "../components/DismissKeyboard";
import { FEED_PHOTO } from "../fragments";

const UPLOAD_PHOTO_MUTATION = gql`
  mutation uploadPhoto($file: Upload!, $caption: String) {
    uploadPhoto(file: $file, caption: $caption) {
      ...FeedPhoto
    }
  }
  ${FEED_PHOTO}
`;

const Container = styled.View`
  flex: 1;
  background-color: black;
  padding: 0px 50px;
`;
const Photo = styled.Image`
  height: 350px;
`;
const CaptionContainer = styled.View`
  margin-top: 30px;
`;
const Caption = styled.TextInput`
  background-color: white;
  color: black;
  padding: 10px 20px;
  border-radius: 100px;
`;

const HeaderRightText = styled.Text`
  color: ${colors.blue};
  font-size: 16px;
  font-weight: 600;
  margin-right: 7px;
`;

export default function UploadForm({ route, navigation }) {
  const updateUploadPhoto = (cache, result) => {
    const {
      data: { uploadPhoto },
    } = result;
    if (uploadPhoto.id) {
      cache.modify({
        id: "ROOT_QUERY",
        fields: {
          seeFeed(prev) {
            return [uploadPhoto, ...prev];
          },
        },
      });
      navigation.navigate("Tabs");
    }
  };
  const [uploadPhotoMutation, { loading }] = useMutation(
    UPLOAD_PHOTO_MUTATION,
    {
      update: updateUploadPhoto,
    }
  );
  const HeaderRight = () => (
    <TouchableOpacity onPress={handleSubmit(onValid)}>
      <HeaderRightText>Next</HeaderRightText>
    </TouchableOpacity>
  );
  const HeaderRightLoading = () => (
    <ActivityIndicator size="small" color="white" style={{ marginRight: 10 }} />
  );
  const { register, handleSubmit, setValue } = useForm();
  useEffect(() => {
    register("caption");
  }, [register]);
  useEffect(() => {
    navigation.setOptions({
      headerRight: loading ? HeaderRightLoading : HeaderRight,
      ...(loading && { headerLeft: () => null }),
    });
  }, [loading]);
  const onValid = ({ caption }) => {
    const file = new ReactNativeFile({
      uri: route.params.file,
      name: `1.jpg`,
      type: "image/jpeg",
    });
    uploadPhotoMutation({
      variables: {
        caption,
        file,
      },
    });
  };
  return (
    <DismissKeyboard>
      <Container>
        <Photo resizeMode="contain" source={{ uri: route.params.file }} />
        <CaptionContainer>
          <Caption
            returnKeyType="done"
            placeholder="Write a caption..."
            placeholderTextColor="rgba(0, 0, 0, 0.5)"
            onSubmitEditing={handleSubmit(onValid)}
            onChangeText={(text) => setValue("caption", text)}
          />
        </CaptionContainer>
      </Container>
    </DismissKeyboard>
  );
}

1) mutation을 통해 사진 업로드 후, 해당 uploadPhoto 데이터가 seeFeed 화면에 바로 cache로 보여 주고, 화면도 이동하였다.

 

2) route를 통해 params (file 명) 을 받아와서 이미지를 상단에 보여주었다.

 

3) 파일 업로드를 위해서는, ReactNativeFile을 사용 해야 한다. 

 

-끝-

 

'코딩강의 > 인스타그램클론(expo-노마드코더)' 카테고리의 다른 글

DEPLOYMENT  (0) 2021.08.01
DIRECT MESSAGES  (0) 2021.07.28
LIKES, SEARCH AND PHOTO 화면  (0) 2021.07.20
FEED  (0) 2021.07.07
AUTHENTICATION  (0) 2021.06.28